/* eslint-disable indent, unicorn/no-fn-reference-in-iterator */
/* globals jQuery, jsPDF */
/**
 * Numerous tools for working with the editor's "canvas"
 * @module svgcanvas
 *
 * @license MIT
 *
 * @copyright 2010 Alexis Deveria, 2010 Pavol Rusnak, 2010 Jeff Schiller
 *
 */

/* Dependencies:
1. Also expects jQuery UI for `svgCanvasToString` and
`convertToGroup` use of `:data()` selector
*/

// Todo: Obtain/adapt latest jsPDF to utilize ES Module for `jsPDF`/avoid global

import './svgpathseg.js'
import jQueryPluginSVG from './jQuery.attr.js' // Needed for SVG attribute setting and array form with `attr`
import jQueryPluginDBox from './dbox.js'
import * as draw from './draw.js' // eslint-disable-line import/no-duplicates
// eslint-disable-next-line no-duplicate-imports
import {
    identifyLayers,
    createLayer,
    cloneLayer,
    deleteCurrentLayer,
    setCurrentLayer,
    renameCurrentLayer,
    setCurrentLayerPosition,
    setLayerVisibility,
    moveSelectedToLayer,
    mergeLayer,
    mergeAllLayers,
    leaveContext,
    setContext
} from './draw.js' // eslint-disable-line import/no-duplicates
import * as pathModule from './path.js'
import { sanitizeSvg } from './sanitize.js'
import { getReverseNS, NS } from './namespaces.js'
import {
    importSetGlobal,
    importScript
} from './external/dynamic-import-polyfill/importModule.js'
import {
    text2xml,
    assignAttributes,
    cleanupElement,
    getElem,
    getUrlFromAttr,
    findDefs,
    getHref,
    setHref,
    getRefElem,
    getRotationAngle,
    getPathBBox,
    preventClickDefault,
    snapToGrid,
    walkTree,
    walkTreePost,
    getBBoxOfElementAsPath,
    convertToPath,
    toXml,
    encode64,
    decode64,
    dataURLToObjectURL,
    createObjectURL,
    getVisibleElements,
    dropXMLInteralSubset,
    init as utilsInit,
    getBBox as utilsGetBBox,
    getStrokedBBoxDefaultVisible,
    isNullish
} from './utilities.js'
import * as hstry from './history.js'
import {
    transformPoint,
    matrixMultiply,
    hasMatrixTransform,
    transformListToTransform,
    getMatrix,
    snapToAngle,
    isIdentity,
    rectsIntersect,
    transformBox
} from './math.js'
import {
    convertToNum,
    convertAttrs,
    convertUnit,
    shortFloat,
    getTypeMap,
    init as unitsInit
} from './units.js'
import {
    isGecko,
    isChrome,
    isIE,
    isWebkit,
    supportsNonScalingStroke,
    supportsGoodTextCharPos
} from './browser.js' // , supportsEditableText
import {
    getTransformList,
    resetListMap,
    SVGTransformList as SVGEditTransformList
} from './svgtransformlist.js'
import { remapElement, init as coordsInit } from './coords.js'
import {
    recalculateDimensions,
    init as recalculateInit
} from './recalculate.js'
import { getSelectorManager, Selector, init as selectInit } from './select.js'

let $ = jQueryPluginSVG(jQuery)
const {
    MoveElementCommand,
    InsertElementCommand,
    RemoveElementCommand,
    ChangeElementCommand,
    BatchCommand,
    UndoManager,
    HistoryEventTypes
} = hstry

if (!window.console) {
    window.console = {}
    window.console.log = function(str) {
        /* */
    }
    window.console.dir = function(str) {
        /* */
    }
}

if (window.opera) {
    window.console.log = function(str) {
        window.opera.postError(str)
    }
    window.console.dir = function(str) {
        /* */
    }
}

// Reenable after fixing eslint-plugin-jsdoc to handle
/**
 * The main SvgCanvas class that manages all SVG-related functions.
 * @memberof module:svgcanvas
 *
 * @borrows module:coords.remapElement as #remapElement
 * @borrows module:recalculate.recalculateDimensions as #recalculateDimensions
 *
 * @borrows module:utilities.cleanupElement as #cleanupElement
 * @borrows module:utilities.getStrokedBBoxDefaultVisible as #getStrokedBBox
 * @borrows module:utilities.getVisibleElements as #getVisibleElements
 * @borrows module:utilities.findDefs as #findDefs
 * @borrows module:utilities.getUrlFromAttr as #getUrlFromAttr
 * @borrows module:utilities.getHref as #getHref
 * @borrows module:utilities.setHref as #setHref
 * @borrows module:utilities.getRotationAngle as #getRotationAngle
 * @borrows module:utilities.getBBox as #getBBox
 * @borrows module:utilities.getElem as #getElem
 * @borrows module:utilities.getRefElem as #getRefElem
 * @borrows module:utilities.assignAttributes as #assignAttributes
 *
 * @borrows module:SVGTransformList.getTransformList as #getTransformList
 * @borrows module:math.matrixMultiply as #matrixMultiply
 * @borrows module:math.hasMatrixTransform as #hasMatrixTransform
 * @borrows module:math.transformListToTransform as #transformListToTransform
 * @borrows module:units.convertToNum as #convertToNum
 * @borrows module:sanitize.sanitizeSvg as #sanitizeSvg
 * @borrows module:path.pathActions.linkControlPoints as #linkControlPoints
 */
class SvgCanvas {
    /**
     * @param {HTMLElement} container - The container HTML element that should hold the SVG root element
     * @param {module:SVGEditor.curConfig} config - An object that contains configuration data
     */
    constructor(container, config) {
        // Alias Namespace constants

        // Default configuration options
        const curConfig = {
            show_outside_canvas: true,
            selectNew: true,
            dimensions: [640, 480]
        }

        // Update config with new one if given
        if (config) {
            $.extend(curConfig, config)
        }

        // Array with width/height of canvas
        const { dimensions } = curConfig

        const canvas = this // eslint-disable-line consistent-this

        // "document" element associated with the container (same as window.document using default svg-editor.js)
        // NOTE: This is not actually a SVG document, but an HTML document.
        const svgdoc = container.ownerDocument

        // This is a container for the document being edited, not the document itself.
        /**
         * @name module:svgcanvas~svgroot
         * @type {SVGSVGElement}
         */
        const svgroot = svgdoc.importNode(
            text2xml(
                '<svg id="svgroot" xmlns="' +
                    NS.SVG +
                    '" xlinkns="' +
                    NS.XLINK +
                    '" ' +
                    'width="' +
                    dimensions[0] +
                    '" height="' +
                    dimensions[1] +
                    '" x="' +
                    dimensions[0] +
                    '" y="' +
                    dimensions[1] +
                    '" overflow="visible">' +
                    '<defs>' +
                    '<filter id="canvashadow" filterUnits="objectBoundingBox">' +
                    '<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>' +
                    '<feOffset in="blur" dx="5" dy="5" result="offsetBlur"/>' +
                    '<feMerge>' +
                    '<feMergeNode in="offsetBlur"/>' +
                    '<feMergeNode in="SourceGraphic"/>' +
                    '</feMerge>' +
                    '</filter>' +
                    '</defs>' +
                    '</svg>'
            ).documentElement,
            true
        )
        container.append(svgroot)

        /**
         * The actual element that represents the final output SVG element
         * @name module:svgcanvas~svgcontent
         * @type {SVGSVGElement}
         */
        let svgcontent = svgdoc.createElementNS(NS.SVG, 'svg')

        /**
         * This function resets the svgcontent element while keeping it in the DOM.
         * @function module:svgcanvas.SvgCanvas#clearSvgContentElement
         * @returns {void}
         */
        const clearSvgContentElement = (canvas.clearSvgContentElement = function() {
            $(svgcontent).empty()

            // TODO: Clear out all other attributes first?
            $(svgcontent)
                .attr({
                    id: 'svgcontent',
                    width: dimensions[0],
                    height: dimensions[1],
                    x: dimensions[0],
                    y: dimensions[1],
                    overflow: curConfig.show_outside_canvas
                        ? 'visible'
                        : 'hidden',
                    xmlns: NS.SVG,
                    'xmlns:se': NS.SE,
                    'xmlns:xlink': NS.XLINK
                })
                .appendTo(svgroot)

            // TODO: make this string optional and set by the client
            const comment = svgdoc.createComment(
                ' Created with SVG-edit - https://github.com/SVG-Edit/svgedit'
            )
            svgcontent.append(comment)
        })
        clearSvgContentElement()

        // Prefix string for element IDs
        let idprefix = 'svg_'

        /**
         * Changes the ID prefix to the given value.
         * @function module:svgcanvas.SvgCanvas#setIdPrefix
         * @param {string} p - String with the new prefix
         * @returns {void}
         */
        canvas.setIdPrefix = function(p) {
            idprefix = p
        }

        /**
         * Current draw.Drawing object
         * @type {module:draw.Drawing}
         * @name module:svgcanvas.SvgCanvas#current_drawing_
         */
        canvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix)

        /**
         * Returns the current Drawing.
         * @name module:svgcanvas.SvgCanvas#getCurrentDrawing
         * @type {module:draw.DrawCanvasInit#getCurrentDrawing}
         */
        const getCurrentDrawing = (canvas.getCurrentDrawing = function() {
            return canvas.current_drawing_
        })

        /**
         * Float displaying the current zoom level (1 = 100%, .5 = 50%, etc)
         * @type {Float}
         */
        let currentZoom = 1

        // pointer to current group (for in-group editing)
        let currentGroup = null

        // Object containing data for the currently selected styles
        const allProperties = {
            shape: {
                fill:
                    (curConfig.initFill.color === 'none' ? '' : '#') +
                    curConfig.initFill.color,
                fill_paint: null,
                fill_opacity: curConfig.initFill.opacity,
                stroke: '#' + curConfig.initStroke.color,
                stroke_paint: null,
                stroke_opacity: curConfig.initStroke.opacity,
                stroke_width: curConfig.initStroke.width,
                stroke_dasharray: 'none',
                stroke_linejoin: 'miter',
                stroke_linecap: 'butt',
                opacity: curConfig.initOpacity
            }
        }

        allProperties.text = $.extend(true, {}, allProperties.shape)
        $.extend(allProperties.text, {
            fill: '#000000',
            stroke_width: curConfig.text && curConfig.text.stroke_width,
            font_size: curConfig.text && curConfig.text.font_size,
            font_family: curConfig.text && curConfig.text.font_family
        })

        // Current shape style properties
        const curShape = allProperties.shape

        // Array with all the currently selected elements
        // default size of 1 until it needs to grow bigger
        let selectedElements = []

        let drawAttrs = {}
        /**
         * @typedef {PlainObject} module:svgcanvas.SVGAsJSON
         * @property {string} element
         * @property {PlainObject<string, string>} attr
         * @property {module:svgcanvas.SVGAsJSON[]} children
         */

        /**
         * @function module:svgcanvas.SvgCanvas#getContentElem
         * @param {Text|Element} data
         * @returns {module:svgcanvas.SVGAsJSON}
         */
        const getJsonFromSvgElement = (this.getJsonFromSvgElement = function(
            data
        ) {
            // Text node
            if (data.nodeType === 3) return data.nodeValue

            const retval = {
                element: data.tagName,
                // namespace: nsMap[data.namespaceURI],
                attr: {},
                children: []
            }

            // Iterate attributes
            for (let i = 0, attr; (attr = data.attributes[i]); i++) {
                if (
                    attr.name !== 'room_id' &&
                    attr.name !== 'room_parent_id' &&
                    attr.name !== 'room_text_id'
                ) {
                    retval.attr[attr.name] = attr.value
                }
            }

            // Iterate children
            for (let i = 0, node; (node = data.childNodes[i]); i++) {
                retval.children[i] = getJsonFromSvgElement(node)
            }

            return retval
        })

        /**
         * This should really be an intersection implementing all rather than a union.
         * @name module:svgcanvas.SvgCanvas#addSVGElementFromJson
         * @type {module:utilities.EditorContext#addSVGElementFromJson|module:path.EditorContext#addSVGElementFromJson}
         */
        const addSVGElementFromJson = (this.addSVGElementFromJson = function(
            data
        ) {
            if (typeof data === 'string') return svgdoc.createTextNode(data)

            let shape = getElem(data.attr.id)
            // if shape is a path but we need to create a rect/ellipse, then remove the path
            const currentLayer = getCurrentDrawing().getCurrentLayer()
            if (shape && data.element !== shape.tagName) {
                shape.remove()
                shape = null
            }
            if (!shape) {
                const ns = data.namespace || NS.SVG
                shape = svgdoc.createElementNS(ns, data.element)
                if (currentLayer) {
                    ;(currentGroup || currentLayer).append(shape)
                }
            }
            if (data.curStyles) {
                assignAttributes(
                    shape,
                    {
                        fill: curShape.fill,
                        stroke: curShape.stroke,
                        'stroke-width': curShape.stroke_width,
                        'stroke-dasharray': curShape.stroke_dasharray,
                        'stroke-linejoin': curShape.stroke_linejoin,
                        'stroke-linecap': curShape.stroke_linecap,
                        'stroke-opacity': curShape.stroke_opacity,
                        'fill-opacity': curShape.fill_opacity,
                        opacity: curShape.opacity / 2,
                        style: 'pointer-events:inherit'
                    },
                    100
                )
            }
            assignAttributes(shape, data.attr, 100)
            cleanupElement(shape)

            // Children
            if (data.children) {
                data.children.forEach(child => {
                    shape.append(addSVGElementFromJson(child))
                })
            }

            return shape
        })

        canvas.getTransformList = getTransformList

        canvas.matrixMultiply = matrixMultiply
        canvas.hasMatrixTransform = hasMatrixTransform
        canvas.transformListToTransform = transformListToTransform

        /**
         * @type {module:utilities.EditorContext#getBaseUnit}
         */
        const getBaseUnit = () => {
            return curConfig.baseUnit
        }

        /**
         * initialize from units.js.
         * Send in an object implementing the ElementContainer interface (see units.js)
         */
        unitsInit(
            /**
             * @implements {module:units.ElementContainer}
             */
            {
                getBaseUnit,
                getElement: getElem,
                getHeight() {
                    return svgcontent.getAttribute('height') / currentZoom
                },
                getWidth() {
                    return svgcontent.getAttribute('width') / currentZoom
                },
                getRoundDigits() {
                    return saveOptions.round_digits
                }
            }
        )

        canvas.convertToNum = convertToNum

        /**
         * This should really be an intersection implementing all rather than a union.
         * @type {module:draw.DrawCanvasInit#getSVGContent|module:utilities.EditorContext#getSVGContent}
         */
        const getSVGContent = () => {
            return svgcontent
        }

        /**
         * Should really be an intersection with all needing to apply rather than a union.
         * @name module:svgcanvas.SvgCanvas#getSelectedElements
         * @type {module:utilities.EditorContext#getSelectedElements|module:draw.DrawCanvasInit#getSelectedElements|module:path.EditorContext#getSelectedElements}
         */
        const getSelectedElements = (this.getSelectedElems = function() {
            return selectedElements
        })

        const { pathActions } = pathModule

        /**
         * This should actually be an intersection as all interfaces should be met.
         * @type {module:utilities.EditorContext#getSVGRoot|module:recalculate.EditorContext#getSVGRoot|module:coords.EditorContext#getSVGRoot|module:path.EditorContext#getSVGRoot}
         */
        const getSVGRoot = () => svgroot

        utilsInit(
            /**
             * @implements {module:utilities.EditorContext}
             */
            {
                pathActions, // Ok since not modifying
                getSVGContent,
                addSVGElementFromJson,
                getSelectedElements,
                getDOMDocument() {
                    return svgdoc
                },
                getDOMContainer() {
                    return container
                },
                getSVGRoot,
                // TODO: replace this mostly with a way to get the current drawing.
                getBaseUnit,
                getSnappingStep() {
                    return curConfig.snappingStep
                }
            }
        )

        canvas.findDefs = findDefs
        canvas.getUrlFromAttr = getUrlFromAttr
        canvas.getHref = getHref
        canvas.setHref = setHref
        /* const getBBox = */ canvas.getBBox = utilsGetBBox
        canvas.getRotationAngle = getRotationAngle
        canvas.getElem = getElem
        canvas.getRefElem = getRefElem
        canvas.assignAttributes = assignAttributes

        this.cleanupElement = cleanupElement

        /**
         * This should actually be an intersection not a union as all should apply.
         * @type {module:coords.EditorContext#getGridSnapping|module:path.EditorContext#getGridSnapping}
         */
        const getGridSnapping = () => {
            return curConfig.gridSnapping
        }

        coordsInit(
            /**
             * @implements {module:coords.EditorContext}
             */
            {
                getDrawing() {
                    return getCurrentDrawing()
                },
                getSVGRoot,
                getGridSnapping
            }
        )
        this.remapElement = remapElement

        recalculateInit(
            /**
             * @implements {module:recalculate.EditorContext}
             */
            {
                getSVGRoot,
                getStartTransform() {
                    return startTransform
                },
                setStartTransform(transform) {
                    startTransform = transform
                }
            }
        )
        this.recalculateDimensions = recalculateDimensions

        // import from sanitize.js
        const nsMap = getReverseNS()
        canvas.sanitizeSvg = sanitizeSvg

        /**
         * @name undoMgr
         * @memberof module:svgcanvas.SvgCanvas#
         * @type {module:history.HistoryEventHandler}
         */
        const undoMgr = (canvas.undoMgr = new UndoManager({
            /**
             * @param {string} eventType One of the HistoryEvent types
             * @param {module:history.HistoryCommand} cmd Fulfills the HistoryCommand interface
             * @fires module:svgcanvas.SvgCanvas#event:changed
             * @returns {void}
             */
            handleHistoryEvent(eventType, cmd) {
                const EventTypes = HistoryEventTypes
                // TODO: handle setBlurOffsets.
                if (
                    eventType === EventTypes.BEFORE_UNAPPLY ||
                    eventType === EventTypes.BEFORE_APPLY
                ) {
                    canvas.clearSelection()
                } else if (
                    eventType === EventTypes.AFTER_APPLY ||
                    eventType === EventTypes.AFTER_UNAPPLY
                ) {
                    const elems = cmd.elements()
                    canvas.pathActions.clear()
                    call('changed', elems)
                    const cmdType = cmd.type()
                    const isApply = eventType === EventTypes.AFTER_APPLY
                    if (cmdType === MoveElementCommand.type()) {
                        const parent = isApply ? cmd.newParent : cmd.oldParent
                        if (parent === svgcontent) {
                            draw.identifyLayers()
                        }
                    } else if (
                        cmdType === InsertElementCommand.type() ||
                        cmdType === RemoveElementCommand.type()
                    ) {
                        if (cmd.parent === svgcontent) {
                            draw.identifyLayers()
                        }
                        if (cmdType === InsertElementCommand.type()) {
                            if (isApply) {
                                restoreRefElems(cmd.elem)
                            }
                        } else if (!isApply) {
                            restoreRefElems(cmd.elem)
                        }
                        if (cmd.elem.tagName === 'use') {
                            setUseData(cmd.elem)
                        }
                    } else if (cmdType === ChangeElementCommand.type()) {
                        // if we are changing layer names, re-identify all layers
                        if (
                            cmd.elem.tagName === 'title' &&
                            cmd.elem.parentNode.parentNode === svgcontent
                        ) {
                            draw.identifyLayers()
                        }
                        const values = isApply ? cmd.newValues : cmd.oldValues
                        // If stdDeviation was changed, update the blur.
                        if (values.stdDeviation) {
                            canvas.setBlurOffsets(
                                cmd.elem.parentNode,
                                values.stdDeviation
                            )
                        }
                        // This is resolved in later versions of webkit, perhaps we should
                        // have a featured detection for correct 'use' behavior?
                        // ——————————
                        // Remove & Re-add hack for Webkit (issue 775)
                        // if (cmd.elem.tagName === 'use' && isWebkit()) {
                        //  const {elem} = cmd;
                        //  if (!elem.getAttribute('x') && !elem.getAttribute('y')) {
                        //    const parent = elem.parentNode;
                        //    const sib = elem.nextSibling;
                        //    elem.remove();
                        //    parent.insertBefore(elem, sib);
                        //    // Ok to replace above with this? `sib.before(elem);`
                        //  }
                        // }
                    }
                }
            }
        }))

        /**
         * This should really be an intersection applying to all types rather than a union.
         * @name module:svgcanvas~addCommandToHistory
         * @type {module:path.EditorContext#addCommandToHistory|module:draw.DrawCanvasInit#addCommandToHistory}
         */
        const addCommandToHistory = function(cmd) {
            canvas.undoMgr.addCommandToHistory(cmd)
        }

        /**
         * This should really be an intersection applying to all types rather than a union.
         * @name module:svgcanvas.SvgCanvas#getZoom
         * @type {module:path.EditorContext#getCurrentZoom|module:select.SVGFactory#getCurrentZoom}
         */
        const getCurrentZoom = (this.getZoom = function() {
            return currentZoom
        })

        /**
         * This method rounds the incoming value to the nearest value based on the `currentZoom`
         * @name module:svgcanvas.SvgCanvas#round
         * @type {module:path.EditorContext#round}
         */
        const round = (this.round = function(val) {
            return parseInt(val * currentZoom) / currentZoom
        })

        selectInit(
            curConfig,
            /**
             * Export to select.js
             * @implements {module:select.SVGFactory}
             */
            {
                createSVGElement(jsonMap) {
                    return canvas.addSVGElementFromJson(jsonMap)
                },
                svgRoot() {
                    return svgroot
                },
                svgContent() {
                    return svgcontent
                },
                getCurrentZoom
            }
        )
        /**
         * This object manages selectors for us
         * @name module:svgcanvas.SvgCanvas#selectorManager
         * @type {module:select.SelectorManager}
         */
        const selectorManager = (this.selectorManager = getSelectorManager())

        /**
         * @name module:svgcanvas.SvgCanvas#getNextId
         * @type {module:path.EditorContext#getNextId}
         */
        const getNextId = (canvas.getNextId = function() {
            return getCurrentDrawing().getNextId()
        })

        /**
         * @name module:svgcanvas.SvgCanvas#getId
         * @type {module:path.EditorContext#getId}
         */
        const getId = (canvas.getId = function() {
            return getCurrentDrawing().getId()
        })

        /**
         * The "implements" should really be an intersection applying to all types rather than a union.
         * @name module:svgcanvas.SvgCanvas#call
         * @type {module:draw.DrawCanvasInit#call|module:path.EditorContext#call}
         */
        const call = function(ev, arg) {
            if (events[ev]) {
                return events[ev](window, arg)
            }
            return undefined
        }

        /**
         * Clears the selection. The 'selected' handler is then optionally called.
         * This should really be an intersection applying to all types rather than a union.
         * @name module:svgcanvas.SvgCanvas#clearSelection
         * @type {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection}
         * @fires module:svgcanvas.SvgCanvas#event:selected
         */
        const clearSelection = (this.clearSelection = function(noCall) {
            selectedElements.forEach(elem => {
                if (isNullish(elem)) {
                    return
                }
                selectorManager.releaseSelector(elem)
            })
            selectedElements = []

            if (!noCall) {
                call('selected', selectedElements)
            }
        })

        /**
         * Adds a list of elements to the selection. The 'selected' handler is then called.
         * @name module:svgcanvas.SvgCanvas#addToSelection
         * @type {module:path.EditorContext#addToSelection}
         * @fires module:svgcanvas.SvgCanvas#event:selected
         */
        const addToSelection = (this.addToSelection = function(
            elemsToAdd,
            showGrips
        ) {
            if (!elemsToAdd.length) {
                return
            }
            // find the first null in our selectedElements array

            let j = 0
            while (j < selectedElements.length) {
                if (isNullish(selectedElements[j])) {
                    break
                }
                ++j
            }

            // now add each element consecutively
            let i = elemsToAdd.length
            while (i--) {
                let elem = elemsToAdd[i]
                if (!elem) {
                    continue
                }
                const bbox = utilsGetBBox(elem)
                if (!bbox) {
                    continue
                }

                if (elem.tagName === 'a' && elem.childNodes.length === 1) {
                    // Make "a" element's child be the selected element
                    elem = elem.firstChild
                }

                // if it's not already there, add it
                if (!selectedElements.includes(elem)) {
                    selectedElements[j] = elem

                    // only the first selectedBBoxes element is ever used in the codebase these days
                    // if (j === 0) selectedBBoxes[0] = utilsGetBBox(elem);
                    j++
                    const sel = selectorManager.requestSelector(elem, bbox)

                    if (selectedElements.length > 1) {
                        sel.showGrips(false)
                    }
                }
            }
            call('selected', selectedElements)

            if (showGrips || selectedElements.length === 1) {
                selectorManager
                    .requestSelector(selectedElements[0])
                    .showGrips(true)
            } else {
                selectorManager
                    .requestSelector(selectedElements[0])
                    .showGrips(false)
            }

            // make sure the elements are in the correct order
            // See: https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition

            selectedElements.sort(function(a, b) {
                if (a && b && a.compareDocumentPosition) {
                    return 3 - (b.compareDocumentPosition(a) & 6) // eslint-disable-line no-bitwise
                }
                if (isNullish(a)) {
                    return 1
                }
                return 0
            })

            // Make sure first elements are not null
            while (isNullish(selectedElements[0])) {
                selectedElements.shift(0)
            }
        })

        /**
         * @type {module:path.EditorContext#getOpacity}
         */
        const getOpacity = function() {
            return curShape.opacity
        }

        /**
         * @name module:svgcanvas.SvgCanvas#getMouseTarget
         * @type {module:path.EditorContext#getMouseTarget}
         */
        const getMouseTarget = (this.getMouseTarget = function(evt) {
            if (isNullish(evt)) {
                return null
            }
            let mouseTarget = evt.target

            // if it was a <use>, Opera and WebKit return the SVGElementInstance
            if (mouseTarget.correspondingUseElement) {
                mouseTarget = mouseTarget.correspondingUseElement
            }

            // for foreign content, go up until we find the foreignObject
            // WebKit browsers set the mouse target to the svgcanvas div
            if (
                [NS.MATH, NS.HTML].includes(mouseTarget.namespaceURI) &&
                mouseTarget.id !== 'svgcanvas'
            ) {
                while (mouseTarget.nodeName !== 'foreignObject') {
                    mouseTarget = mouseTarget.parentNode
                    if (!mouseTarget) {
                        return svgroot
                    }
                }
            }

            // Get the desired mouseTarget with jQuery selector-fu
            // If it's root-like, select the root
            const currentLayer = getCurrentDrawing().getCurrentLayer()
            if (
                [svgroot, container, svgcontent, currentLayer].includes(
                    mouseTarget
                )
            ) {
                return svgroot
            }

            const $target = $(mouseTarget)

            // If it's a selection grip, return the grip parent
            if ($target.closest('#selectorParentGroup').length) {
                // While we could instead have just returned mouseTarget,
                // this makes it easier to indentify as being a selector grip
                return selectorManager.selectorParentGroup
            }

            while (mouseTarget.parentNode !== (currentGroup || currentLayer)) {
                mouseTarget = mouseTarget.parentNode
            }

            //
            // // go up until we hit a child of a layer
            // while (mouseTarget.parentNode.parentNode.tagName == 'g') {
            //   mouseTarget = mouseTarget.parentNode;
            // }
            // Webkit bubbles the mouse event all the way up to the div, so we
            // set the mouseTarget to the svgroot like the other browsers
            // if (mouseTarget.nodeName.toLowerCase() == 'div') {
            //   mouseTarget = svgroot;
            // }

            return mouseTarget
        })

        /**
         * @namespace {module:path.pathActions} pathActions
         * @memberof module:svgcanvas.SvgCanvas#
         * @see module:path.pathActions
         */
        canvas.pathActions = pathActions
        /**
         * @type {module:path.EditorContext#resetD}
         */
        function resetD(p) {
            p.setAttribute('d', pathActions.convertPath(p))
        }
        pathModule.init(
            /**
             * @implements {module:path.EditorContext}
             */
            {
                selectorManager, // Ok since not changing
                canvas, // Ok since not changing
                call,
                resetD,
                round,
                clearSelection,
                addToSelection,
                addCommandToHistory,
                remapElement,
                addSVGElementFromJson,
                getGridSnapping,
                getOpacity,
                getSelectedElements,
                getContainer() {
                    return container
                },
                setStarted(s) {
                    started = s
                },
                getRubberBox() {
                    return rubberBox
                },
                setRubberBox(rb) {
                    rubberBox = rb
                    return rubberBox
                },
                /**
                 * @param {PlainObject} ptsInfo
                 * @param {boolean} ptsInfo.closedSubpath
                 * @param {SVGCircleElement[]} ptsInfo.grips
                 * @fires module:svgcanvas.SvgCanvas#event:pointsAdded
                 * @fires module:svgcanvas.SvgCanvas#event:selected
                 * @returns {void}
                 */
                addPtsToSelection({ closedSubpath, grips }) {
                    // TODO: Correct this:
                    pathActions.canDeleteNodes = true
                    pathActions.closed_subpath = closedSubpath
                    call('pointsAdded', { closedSubpath, grips })
                    call('selected', grips)
                },
                /**
                 * @param {PlainObject} changes
                 * @param {ChangeElementCommand} changes.cmd
                 * @param {SVGPathElement} changes.elem
                 * @fires module:svgcanvas.SvgCanvas#event:changed
                 * @returns {void}
                 */
                endChanges({ cmd, elem }) {
                    addCommandToHistory(cmd)
                    call('changed', [elem])
                },
                getCurrentZoom,
                getId,
                getNextId,
                getMouseTarget,
                getCurrentMode() {
                    return currentMode
                },
                setCurrentMode(cm) {
                    currentMode = cm
                    return currentMode
                },
                getDrawnPath() {
                    return drawnPath
                },
                setDrawnPath(dp) {
                    drawnPath = dp
                    return drawnPath
                },
                getSVGRoot
            }
        )

        // Interface strings, usually for title elements
        const uiStrings = {}

        const visElems =
            'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'
        const refAttrs = [
            'clip-path',
            'fill',
            'filter',
            'marker-end',
            'marker-mid',
            'marker-start',
            'mask',
            'stroke'
        ]

        const elData = $.data

        // Animation element to change the opacity of any newly created element
        const opacAni = document.createElementNS(NS.SVG, 'animate')
        $(opacAni)
            .attr({
                attributeName: 'opacity',
                begin: 'indefinite',
                dur: 1,
                fill: 'freeze'
            })
            .appendTo(svgroot)

        const restoreRefElems = function(elem) {
            // Look for missing reference elements, restore any found
            const attrs = $(elem).attr(refAttrs)
            Object.values(attrs).forEach(val => {
                if (val && val.startsWith('url(')) {
                    const id = getUrlFromAttr(val).substr(1)
                    const ref = getElem(id)
                    if (!ref) {
                        findDefs().append(removedElements[id])
                        delete removedElements[id]
                    }
                }
            })

            const childs = elem.getElementsByTagName('*')

            if (childs.length) {
                for (let i = 0, l = childs.length; i < l; i++) {
                    restoreRefElems(childs[i])
                }
            }
        }

        // (function () {
        // TODO For Issue 208: this is a start on a thumbnail
        //  const svgthumb = svgdoc.createElementNS(NS.SVG, 'use');
        //  svgthumb.setAttribute('width', '100');
        //  svgthumb.setAttribute('height', '100');
        //  setHref(svgthumb, '#svgcontent');
        //  svgroot.append(svgthumb);
        // }());

        /**
         * @typedef {PlainObject} module:svgcanvas.SaveOptions
         * @property {boolean} apply
         * @property {"embed"} [image]
         * @property {Integer} round_digits
         */

        // Object to contain image data for raster images that were found encodable
        const encodableImages = {},
            // Object with save options
            /**
             * @type {module:svgcanvas.SaveOptions}
             */
            saveOptions = { round_digits: 5 },
            // Object with IDs for imported files, to see if one was already added
            importIds = {},
            // Current text style properties
            curText = allProperties.text,
            // Object to contain all included extensions
            extensions = {},
            // Map of deleted reference elements
            removedElements = {}

        let // String with image URL of last loadable image
            lastGoodImgUrl = 'images/image-default.png',
            // Boolean indicating whether or not a draw action has been started
            started = false,
            // String with an element's initial transform attribute value
            startTransform = null,
            // String indicating the current editor mode
            currentMode = 'select',
            // String with the current direction in which an element is being resized
            currentResizeMode = 'none',
            // Current general properties
            curProperties = curShape,
            // Array with selected elements' Bounding box object
            // selectedBBoxes = new Array(1),

            // The DOM element that was just selected
            justSelected = null,
            // DOM element for selection rectangle drawn by the user
            rubberBox = null,
            // Array of current BBoxes, used in getIntersectionList().
            curBBoxes = [],
            // Canvas point for the most recent right click
            lastClickPoint = null

        this.runExtension = function(name, action, vars) {
            return this.runExtensions(action, vars, false, n => n === name)
        }
        /**
         * @typedef {module:svgcanvas.ExtensionMouseDownStatus|module:svgcanvas.ExtensionMouseUpStatus|module:svgcanvas.ExtensionIDsUpdatedStatus|module:locale.ExtensionLocaleData[]|void} module:svgcanvas.ExtensionStatus
         * @tutorial ExtensionDocs
         */
        /**
         * @callback module:svgcanvas.ExtensionVarBuilder
         * @param {string} name The name of the extension
         * @returns {module:svgcanvas.SvgCanvas#event:ext_addLangData}
         */
        /**
         * @callback module:svgcanvas.ExtensionNameFilter
         * @param {string} name
         * @returns {boolean}
         */
        /**
         * @todo Consider: Should this return an array by default, so extension results aren't overwritten?
         * @todo Would be easier to document if passing in object with key of action and vars as value; could then define an interface which tied both together
         * @function module:svgcanvas.SvgCanvas#runExtensions
         * @param {"mouseDown"|"mouseMove"|"mouseUp"|"zoomChanged"|"IDsUpdated"|"canvasUpdated"|"toolButtonStateUpdate"|"selectedChanged"|"elementTransition"|"elementChanged"|"langReady"|"langChanged"|"addLangData"|"onNewDocument"|"workareaResized"} action
         * @param {module:svgcanvas.SvgCanvas#event:ext_mouseDown|module:svgcanvas.SvgCanvas#event:ext_mouseMove|module:svgcanvas.SvgCanvas#event:ext_mouseUp|module:svgcanvas.SvgCanvas#event:ext_zoomChanged|module:svgcanvas.SvgCanvas#event:ext_IDsUpdated|module:svgcanvas.SvgCanvas#event:ext_canvasUpdated|module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate|module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementTransition|module:svgcanvas.SvgCanvas#event:ext_elementChanged|module:svgcanvas.SvgCanvas#event:ext_langReady|module:svgcanvas.SvgCanvas#event:ext_langChanged|module:svgcanvas.SvgCanvas#event:ext_addLangData|module:svgcanvas.SvgCanvas#event:ext_onNewDocument|module:svgcanvas.SvgCanvas#event:ext_workareaResized|module:svgcanvas.ExtensionVarBuilder} [vars]
         * @param {boolean} [returnArray]
         * @param {module:svgcanvas.ExtensionNameFilter} nameFilter
         * @returns {GenericArray<module:svgcanvas.ExtensionStatus>|module:svgcanvas.ExtensionStatus|false} See {@tutorial ExtensionDocs} on the ExtensionStatus.
         */
        const runExtensions = (this.runExtensions = function(
            action,
            vars,
            returnArray,
            nameFilter
        ) {
            let result = returnArray ? [] : false
            $.each(extensions, function(name, ext) {
                if (nameFilter && !nameFilter(name)) {
                    return
                }
                if (ext && action in ext) {
                    if (typeof vars === 'function') {
                        vars = vars(name) // ext, action
                    }
                    if (returnArray) {
                        result.push(ext[action](vars))
                    } else {
                        result = ext[action](vars)
                    }
                }
            })
            return result
        })

        /**
         * @typedef {PlainObject} module:svgcanvas.ExtensionMouseDownStatus
         * @property {boolean} started Indicates that creating/editing has started
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.ExtensionMouseUpStatus
         * @property {boolean} keep Indicates if the current element should be kept
         * @property {boolean} started Indicates if editing should still be considered as "started"
         * @property {Element} element The element being affected
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.ExtensionIDsUpdatedStatus
         * @property {string[]} remove Contains string IDs (used by `ext-connector.js`)
         */

        /**
         * @interface module:svgcanvas.ExtensionInitResponse
         * @property {module:SVGEditor.ContextTool[]|PlainObject<string, module:SVGEditor.ContextTool>} [context_tools]
         * @property {module:SVGEditor.Button[]|PlainObject<Integer, module:SVGEditor.Button>} [buttons]
         * @property {string} [svgicons] The location of a local SVG or SVGz file
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#mouseDown
         * @param {module:svgcanvas.SvgCanvas#event:ext_mouseDown} arg
         * @returns {void|module:svgcanvas.ExtensionMouseDownStatus}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#mouseMove
         * @param {module:svgcanvas.SvgCanvas#event:ext_mouseMove} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#mouseUp
         * @param {module:svgcanvas.SvgCanvas#event:ext_mouseUp} arg
         * @returns {module:svgcanvas.ExtensionMouseUpStatus}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#zoomChanged
         * @param {module:svgcanvas.SvgCanvas#event:ext_zoomChanged} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#IDsUpdated
         * @param {module:svgcanvas.SvgCanvas#event:ext_IDsUpdated} arg
         * @returns {module:svgcanvas.ExtensionIDsUpdatedStatus}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#canvasUpdated
         * @param {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#toolButtonStateUpdate
         * @param {module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#selectedChanged
         * @param {module:svgcanvas.SvgCanvas#event:ext_selectedChanged} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#elementTransition
         * @param {module:svgcanvas.SvgCanvas#event:ext_elementTransition} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#elementChanged
         * @param {module:svgcanvas.SvgCanvas#event:ext_elementChanged} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#langReady
         * @param {module:svgcanvas.SvgCanvas#event:ext_langReady} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#langChanged
         * @param {module:svgcanvas.SvgCanvas#event:ext_langChanged} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#addLangData
         * @param {module:svgcanvas.SvgCanvas#event:ext_addLangData} arg
         * @returns {Promise<module:locale.ExtensionLocaleData>} Resolves to {@link module:locale.ExtensionLocaleData}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#onNewDocument
         * @param {module:svgcanvas.SvgCanvas#event:ext_onNewDocument} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#workareaResized
         * @param {module:svgcanvas.SvgCanvas#event:ext_workareaResized} arg
         * @returns {void}
         */
        /**
         * @function module:svgcanvas.ExtensionInitResponse#callback
         * @this module:SVGEditor
         * @param {module:svgcanvas.SvgCanvas#event:ext_callback} arg
         * @returns {void}
         */

        /**
         * @callback module:svgcanvas.ExtensionInitCallback
         * @this module:SVGEditor
         * @param {module:svgcanvas.ExtensionArgumentObject} arg
         * @returns {Promise<module:svgcanvas.ExtensionInitResponse|void>} Resolves to [ExtensionInitResponse]{@link module:svgcanvas.ExtensionInitResponse} or `undefined`
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.ExtensionInitArgs
         * @param {external:jQuery} initArgs.$
         * @param {module:SVGEditor~ImportLocale} initArgs.importLocale
         */
        /**
         * Add an extension to the editor.
         * @function module:svgcanvas.SvgCanvas#addExtension
         * @param {string} name - String with the ID of the extension. Used internally; no need for i18n.
         * @param {module:svgcanvas.ExtensionInitCallback} [extInitFunc] - Function supplied by the extension with its data
         * @param {module:svgcanvas.ExtensionInitArgs} initArgs
         * @fires module:svgcanvas.SvgCanvas#event:extension_added
         * @throws {TypeError|Error} `TypeError` if `extInitFunc` is not a function, `Error`
         *   if extension of supplied name already exists
         * @returns {Promise<void>} Resolves to `undefined`
         */
        this.addExtension = async function(
            name,
            extInitFunc,
            { $: jq, importLocale }
        ) {
            if (typeof extInitFunc !== 'function') {
                throw new TypeError(
                    'Function argument expected for `svgcanvas.addExtension`'
                )
            }
            if (name in extensions) {
                throw new Error(
                    'Cannot add extension "' +
                        name +
                        '", an extension by that name already exists.'
                )
            }
            // Provide private vars/funcs here. Is there a better way to do this?
            /**
             * @typedef {module:svgcanvas.PrivateMethods} module:svgcanvas.ExtensionArgumentObject
             * @property {SVGSVGElement} svgroot See {@link module:svgcanvas~svgroot}
             * @property {SVGSVGElement} svgcontent See {@link module:svgcanvas~svgcontent}
             * @property {!(string|Integer)} nonce See {@link module:draw.Drawing#getNonce}
             * @property {module:select.SelectorManager} selectorManager
             * @property {module:SVGEditor~ImportLocale} importLocale
             */
            /**
             * @type {module:svgcanvas.ExtensionArgumentObject}
             * @see {@link module:svgcanvas.PrivateMethods} source for the other methods/properties
             */
            const argObj = $.extend(canvas.getPrivateMethods(), {
                $: jq,
                importLocale,
                svgroot,
                svgcontent,
                nonce: getCurrentDrawing().getNonce(),
                selectorManager
            })
            const extObj = await extInitFunc(argObj)
            if (extObj) {
                extObj.name = name
            }

            // eslint-disable-next-line require-atomic-updates
            extensions[name] = extObj
            return call('extension_added', extObj)
        }

        /**
         * This method sends back an array or a NodeList full of elements that
         * intersect the multi-select rubber-band-box on the currentLayer only.
         *
         * We brute-force `getIntersectionList` for browsers that do not support it (Firefox).
         *
         * Reference:
         * Firefox does not implement `getIntersectionList()`, see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=501421}.
         * @function module:svgcanvas.SvgCanvas#getIntersectionList
         * @param {SVGRect} rect
         * @returns {Element[]|NodeList} Bbox elements
         */
        const getIntersectionList = (this.getIntersectionList = function(rect) {
            if (isNullish(rubberBox)) {
                return null
            }

            const parent = currentGroup || getCurrentDrawing().getCurrentLayer()

            let rubberBBox
            if (!rect) {
                rubberBBox = rubberBox.getBBox()
                const bb = svgcontent.createSVGRect()

                ;[
                    'x',
                    'y',
                    'width',
                    'height',
                    'top',
                    'right',
                    'bottom',
                    'left'
                ].forEach(o => {
                    bb[o] = rubberBBox[o] / currentZoom
                })
                rubberBBox = bb
            } else {
                rubberBBox = svgcontent.createSVGRect()
                rubberBBox.x = rect.x
                rubberBBox.y = rect.y
                rubberBBox.width = rect.width
                rubberBBox.height = rect.height
            }

            let resultList = null
            if (!isIE()) {
                if (typeof svgroot.getIntersectionList === 'function') {
                    // Offset the bbox of the rubber box by the offset of the svgcontent element.
                    rubberBBox.x += parseInt(svgcontent.getAttribute('x'))
                    rubberBBox.y += parseInt(svgcontent.getAttribute('y'))

                    resultList = svgroot.getIntersectionList(rubberBBox, parent)
                }
            }

            if (
                isNullish(resultList) ||
                typeof resultList.item !== 'function'
            ) {
                resultList = []

                if (!curBBoxes.length) {
                    // Cache all bboxes
                    curBBoxes = getVisibleElementsAndBBoxes(parent)
                }
                let i = curBBoxes.length
                while (i--) {
                    if (!rubberBBox.width) {
                        continue
                    }
                    if (rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {
                        resultList.push(curBBoxes[i].elem)
                    }
                }
            }

            // addToSelection expects an array, but it's ok to pass a NodeList
            // because using square-bracket notation is allowed:
            // https://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
            return resultList
        })

        this.getStrokedBBox = getStrokedBBoxDefaultVisible

        this.getVisibleElements = getVisibleElements

        /**
         * @typedef {PlainObject} ElementAndBBox
         * @property {Element} elem - The element
         * @property {module:utilities.BBoxObject} bbox - The element's BBox as retrieved from `getStrokedBBoxDefaultVisible`
         */

        /**
         * Get all elements that have a BBox (excludes `<defs>`, `<title>`, etc).
         * Note that 0-opacity, off-screen etc elements are still considered "visible"
         * for this function.
         * @function module:svgcanvas.SvgCanvas#getVisibleElementsAndBBoxes
         * @param {Element} parent - The parent DOM element to search within
         * @returns {ElementAndBBox[]} An array with objects that include:
         */
        const getVisibleElementsAndBBoxes = (this.getVisibleElementsAndBBoxes = function(
            parent
        ) {
            if (!parent) {
                parent = $(svgcontent).children() // Prevent layers from being included
            }
            const contentElems = []
            $(parent)
                .children()
                .each(function(i, elem) {
                    if (elem.getBBox) {
                        contentElems.push({
                            elem,
                            bbox: getStrokedBBoxDefaultVisible([elem])
                        })
                    }
                })
            return contentElems.reverse()
        })

        /**
         * Wrap an SVG element into a group element, mark the group as 'gsvg'.
         * @function module:svgcanvas.SvgCanvas#groupSvgElem
         * @param {Element} elem - SVG element to wrap
         * @returns {void}
         */
        const groupSvgElem = (this.groupSvgElem = function(elem) {
            const g = document.createElementNS(NS.SVG, 'g')
            elem.replaceWith(g)
            $(g)
                .append(elem)
                .data('gsvg', elem)[0].id = getNextId()
        })

        // Set scope for these functions

        // Object to contain editor event names and callback functions
        const events = {}

        canvas.call = call
        /**
         * Array of what was changed (elements, layers)
         * @event module:svgcanvas.SvgCanvas#event:changed
         * @type {Element[]}
         */
        /**
         * Array of selected elements
         * @event module:svgcanvas.SvgCanvas#event:selected
         * @type {Element[]}
         */
        /**
         * Array of selected elements
         * @event module:svgcanvas.SvgCanvas#event:transition
         * @type {Element[]}
         */
        /**
         * The Element is always `SVGGElement`?
         * If not `null`, will be the set current group element
         * @event module:svgcanvas.SvgCanvas#event:contextset
         * @type {null|Element}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:pointsAdded
         * @type {PlainObject}
         * @property {boolean} closedSubpath
         * @property {SVGCircleElement[]} grips Grips elements
         */

        /**
         * @event module:svgcanvas.SvgCanvas#event:zoomed
         * @type {PlainObject}
         * @property {Float} x
         * @property {Float} y
         * @property {Float} width
         * @property {Float} height
         * @property {0.5|2} factor
         * @see module:SVGEditor.BBoxObjectWithFactor
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:updateCanvas
         * @type {PlainObject}
         * @property {false} center
         * @property {module:math.XYObject} newCtr
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.ExtensionInitResponsePlusName
         * @implements {module:svgcanvas.ExtensionInitResponse}
         * @property {string} name The extension's resolved ID (whether explicit or based on file name)
         */
        /**
         * Generalized extension object response of
         * [`init()`]{@link module:svgcanvas.ExtensionInitCallback}
         * along with the name of the extension.
         * @event module:svgcanvas.SvgCanvas#event:extension_added
         * @type {module:svgcanvas.ExtensionInitResponsePlusName|void}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:extensions_added
         * @type {void}
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.Message
         * @property {any} data The data
         * @property {string} origin The origin
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:message
         * @type {module:svgcanvas.Message}
         */
        /**
         * SVG canvas converted to string
         * @event module:svgcanvas.SvgCanvas#event:saved
         * @type {string}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:setnonce
         * @type {!(string|Integer)}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:unsetnonce
         * @type {void}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:zoomDone
         * @type {void}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:cleared
         * @type {void}
         */

        /**
         * @event module:svgcanvas.SvgCanvas#event:exported
         * @type {module:svgcanvas.ImageExportedResults}
         */
        /**
         * @event module:svgcanvas.SvgCanvas#event:exportedPDF
         * @type {module:svgcanvas.PDFExportedResults}
         */
        /**
         * Creating a cover-all class until {@link https://github.com/jsdoc3/jsdoc/issues/1545} may be supported.
         * `undefined` may be returned by {@link module:svgcanvas.SvgCanvas#event:extension_added} if the extension's `init` returns `undefined` It is also the type for the following events "zoomDone", "unsetnonce", "cleared", and "extensions_added".
         * @event module:svgcanvas.SvgCanvas#event:GenericCanvasEvent
         * @type {module:svgcanvas.SvgCanvas#event:selected|module:svgcanvas.SvgCanvas#event:changed|module:svgcanvas.SvgCanvas#event:contextset|module:svgcanvas.SvgCanvas#event:pointsAdded|module:svgcanvas.SvgCanvas#event:extension_added|module:svgcanvas.SvgCanvas#event:extensions_added|module:svgcanvas.SvgCanvas#event:message|module:svgcanvas.SvgCanvas#event:transition|module:svgcanvas.SvgCanvas#event:zoomed|module:svgcanvas.SvgCanvas#event:updateCanvas|module:svgcanvas.SvgCanvas#event:saved|module:svgcanvas.SvgCanvas#event:exported|module:svgcanvas.SvgCanvas#event:exportedPDF|module:svgcanvas.SvgCanvas#event:setnonce|module:svgcanvas.SvgCanvas#event:unsetnonce|void}
         */

        /**
         * The promise return, if present, resolves to `undefined`
         *  (`extension_added`, `exported`, `saved`)
         * @typedef {Promise<void>|void} module:svgcanvas.EventHandlerReturn
         */

        /**
         * @callback module:svgcanvas.EventHandler
         * @param {external:Window} win
         * @param {module:svgcanvas.SvgCanvas#event:GenericCanvasEvent} arg
         * @listens module:svgcanvas.SvgCanvas#event:GenericCanvasEvent
         * @returns {module:svgcanvas.EventHandlerReturn}
         */

        /**
         * Attaches a callback function to an event.
         * @function module:svgcanvas.SvgCanvas#bind
         * @param {"changed"|"contextset"|"selected"|"pointsAdded"|"extension_added"|"extensions_added"|"message"|"transition"|"zoomed"|"updateCanvas"|"zoomDone"|"saved"|"exported"|"exportedPDF"|"setnonce"|"unsetnonce"|"cleared"} ev - String indicating the name of the event
         * @param {module:svgcanvas.EventHandler} f - The callback function to bind to the event
         * @returns {module:svgcanvas.EventHandler} The previous event
         */
        canvas.bind = function(ev, f) {
            const old = events[ev]
            events[ev] = f
            return old
        }

        /**
         * Runs the SVG Document through the sanitizer and then updates its paths.
         * @function module:svgcanvas.SvgCanvas#prepareSvg
         * @param {XMLDocument} newDoc - The SVG DOM document
         * @returns {void}
         */
        this.prepareSvg = function(newDoc) {
            this.sanitizeSvg(newDoc.documentElement)

            // convert paths into absolute commands
            const paths = [...newDoc.getElementsByTagNameNS(NS.SVG, 'path')]
            paths.forEach(path => {
                path.setAttribute('d', pathActions.convertPath(path))
                pathActions.fixEnd(path)
            })
        }

        /**
         * Hack for Firefox bugs where text element features aren't updated or get
         * messed up. See issue 136 and issue 137.
         * This function clones the element and re-selects it.
         * @function module:svgcanvas~ffClone
         * @todo Test for this bug on load and add it to "support" object instead of
         * browser sniffing
         * @param {Element} elem - The (text) DOM element to clone
         * @returns {Element} Cloned element
         */
        const ffClone = function(elem) {
            if (!isGecko()) {
                return elem
            }
            const clone = elem.cloneNode(true)
            elem.before(clone)
            elem.remove()
            selectorManager.releaseSelector(elem)
            selectedElements[0] = clone
            selectorManager.requestSelector(clone).showGrips(true)
            return clone
        }

        // `this.each` is deprecated, if any extension used this it can be recreated by doing this:
        // * @example $(canvas.getRootElem()).children().each(...)
        // * @function module:svgcanvas.SvgCanvas#each
        // this.each = function (cb) {
        //  $(svgroot).children().each(cb);
        // };

        /**
         * Removes any old rotations if present, prepends a new rotation at the
         * transformed center.
         * @function module:svgcanvas.SvgCanvas#setRotationAngle
         * @param {string|Float} val - The new rotation angle in degrees
         * @param {boolean} preventUndo - Indicates whether the action should be undoable or not
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setRotationAngle = function(val, preventUndo) {
            // ensure val is the proper type
            val = parseFloat(val)
            const elem = selectedElements[0]
            const oldTransform = elem.getAttribute('transform')
            const bbox = utilsGetBBox(elem)
            const cx = bbox.x + bbox.width / 2,
                cy = bbox.y + bbox.height / 2
            const tlist = getTransformList(elem)

            // only remove the real rotational transform if present (i.e. at index=0)
            if (tlist.numberOfItems > 0) {
                const xform = tlist.getItem(0)
                if (xform.type === 4) {
                    tlist.removeItem(0)
                }
            }
            // find Rnc and insert it
            if (val !== 0) {
                const center = transformPoint(
                    cx,
                    cy,
                    transformListToTransform(tlist).matrix
                )
                const Rnc = svgroot.createSVGTransform()
                Rnc.setRotate(val, center.x, center.y)
                if (tlist.numberOfItems) {
                    tlist.insertItemBefore(Rnc, 0)
                } else {
                    tlist.appendItem(Rnc)
                }
            } else if (tlist.numberOfItems === 0) {
                elem.removeAttribute('transform')
            }

            if (!preventUndo) {
                // we need to undo it, then redo it so it can be undo-able! :)
                // TODO: figure out how to make changes to transform list undo-able cross-browser?
                const newTransform = elem.getAttribute('transform')
                elem.setAttribute('transform', oldTransform)
                changeSelectedAttribute(
                    'transform',
                    newTransform,
                    selectedElements
                )
                call('changed', selectedElements)
            }
            // const pointGripContainer = getElem('pathpointgrip_container');
            // if (elem.nodeName === 'path' && pointGripContainer) {
            //   pathActions.setPointContainerTransform(elem.getAttribute('transform'));
            // }
            const selector = selectorManager.requestSelector(
                selectedElements[0]
            )
            selector.resize()
            Selector.updateGripCursors(val)
        }

        /**
         * Runs `recalculateDimensions` on the selected elements,
         * adding the changes to a single batch command.
         * @function module:svgcanvas.SvgCanvas#recalculateAllSelectedDimensions
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        const recalculateAllSelectedDimensions = (this.recalculateAllSelectedDimensions = function() {
            const text = currentResizeMode === 'none' ? 'position' : 'size'
            const batchCmd = new BatchCommand(text)

            let i = selectedElements.length
            while (i--) {
                const elem = selectedElements[i]
                // if (getRotationAngle(elem) && !hasMatrixTransform(getTransformList(elem))) { continue; }
                const cmd = recalculateDimensions(elem)
                if (cmd) {
                    batchCmd.addSubCommand(cmd)
                }
            }

            if (!batchCmd.isEmpty()) {
                addCommandToHistory(batchCmd)
                call('changed', selectedElements)
            }
        })

        /**
         * Debug tool to easily see the current matrix in the browser's console.
         * @function module:svgcanvas~logMatrix
         * @param {SVGMatrix} m The matrix
         * @returns {void}
         */
        const logMatrix = function(m) {
            console.log([m.a, m.b, m.c, m.d, m.e, m.f]) // eslint-disable-line no-console
        }

        // Root Current Transformation Matrix in user units
        let rootSctm = null

        /**
         * Group: Selection
         */

        // TODO: do we need to worry about selectedBBoxes here?

        /**
         * Selects only the given elements, shortcut for `clearSelection(); addToSelection()`.
         * @function module:svgcanvas.SvgCanvas#selectOnly
         * @param {Element[]} elems - an array of DOM elements to be selected
         * @param {boolean} showGrips - Indicates whether the resize grips should be shown
         * @returns {void}
         */
        const selectOnly = (this.selectOnly = function(elems, showGrips) {
            clearSelection(true)
            addToSelection(elems, showGrips)
        })

        // TODO: could use slice here to make this faster?
        // TODO: should the 'selected' handler

        /**
         * Removes elements from the selection.
         * @function module:svgcanvas.SvgCanvas#removeFromSelection
         * @param {Element[]} elemsToRemove - An array of elements to remove from selection
         * @returns {void}
         */
        /* const removeFromSelection = */ this.removeFromSelection = function(
            elemsToRemove
        ) {
            if (isNullish(selectedElements[0])) {
                return
            }
            if (!elemsToRemove.length) {
                return
            }

            // find every element and remove it from our array copy
            const newSelectedItems = [],
                len = selectedElements.length
            for (let i = 0; i < len; ++i) {
                const elem = selectedElements[i]
                if (elem) {
                    // keep the item
                    if (!elemsToRemove.includes(elem)) {
                        newSelectedItems.push(elem)
                    } else {
                        // remove the item and its selector
                        selectorManager.releaseSelector(elem)
                    }
                }
            }
            // the copy becomes the master now
            selectedElements = newSelectedItems
        }

        /**
         * Clears the selection, then adds all elements in the current layer to the selection.
         * @function module:svgcanvas.SvgCanvas#selectAllInCurrentLayer
         * @returns {void}
         */
        this.selectAllInCurrentLayer = function() {
            const currentLayer = getCurrentDrawing().getCurrentLayer()
            if (currentLayer) {
                currentMode = 'select'
                selectOnly($(currentGroup || currentLayer).children())
            }
        }

        let drawnPath = null

        // Mouse events
        ;(function() {
            const freehand = {
                minx: null,
                miny: null,
                maxx: null,
                maxy: null
            }
            const THRESHOLD_DIST = 0.8,
                STEP_COUNT = 10
            let dAttr = null,
                startX = null,
                startY = null,
                rStartX = null,
                rStartY = null,
                initBbox = {},
                sumDistance = 0,
                controllPoint2 = { x: 0, y: 0 },
                controllPoint1 = { x: 0, y: 0 },
                start = { x: 0, y: 0 },
                end = { x: 0, y: 0 },
                bSpline = { x: 0, y: 0 },
                nextPos = { x: 0, y: 0 },
                parameter,
                nextParameter

            const getBsplinePoint = function(t) {
                const spline = { x: 0, y: 0 },
                    p0 = controllPoint2,
                    p1 = controllPoint1,
                    p2 = start,
                    p3 = end,
                    S = 1.0 / 6.0,
                    t2 = t * t,
                    t3 = t2 * t

                const m = [
                    [-1, 3, -3, 1],
                    [3, -6, 3, 0],
                    [-3, 0, 3, 0],
                    [1, 4, 1, 0]
                ]

                spline.x =
                    S *
                    ((p0.x * m[0][0] +
                        p1.x * m[0][1] +
                        p2.x * m[0][2] +
                        p3.x * m[0][3]) *
                        t3 +
                        (p0.x * m[1][0] +
                            p1.x * m[1][1] +
                            p2.x * m[1][2] +
                            p3.x * m[1][3]) *
                            t2 +
                        (p0.x * m[2][0] +
                            p1.x * m[2][1] +
                            p2.x * m[2][2] +
                            p3.x * m[2][3]) *
                            t +
                        (p0.x * m[3][0] +
                            p1.x * m[3][1] +
                            p2.x * m[3][2] +
                            p3.x * m[3][3]))
                spline.y =
                    S *
                    ((p0.y * m[0][0] +
                        p1.y * m[0][1] +
                        p2.y * m[0][2] +
                        p3.y * m[0][3]) *
                        t3 +
                        (p0.y * m[1][0] +
                            p1.y * m[1][1] +
                            p2.y * m[1][2] +
                            p3.y * m[1][3]) *
                            t2 +
                        (p0.y * m[2][0] +
                            p1.y * m[2][1] +
                            p2.y * m[2][2] +
                            p3.y * m[2][3]) *
                            t +
                        (p0.y * m[3][0] +
                            p1.y * m[3][1] +
                            p2.y * m[3][2] +
                            p3.y * m[3][3]))

                return {
                    x: spline.x,
                    y: spline.y
                }
            }
            /**
             * Follows these conditions:
             * - When we are in a create mode, the element is added to the canvas but the
             *   action is not recorded until mousing up.
             * - When we are in select mode, select the element, remember the position
             *   and do nothing else.
             * @param {MouseEvent} evt
             * @fires module:svgcanvas.SvgCanvas#event:ext_mouseDown
             * @returns {void}
             */
            const mouseDown = function(evt) {
                if (canvas.spaceKey || evt.button === 1) {
                    return
                }

                const rightClick = evt.button === 2

                if (evt.altKey) {
                    // duplicate when dragging
                    canvas.cloneSelectedElements(0, 0)
                }

                rootSctm = $('#svgcontent g')[0]
                    .getScreenCTM()
                    .inverse()

                const pt = transformPoint(
                        evt.pageX || 0,
                        evt.pageY || 0,
                        rootSctm
                    ),
                    mouseX = pt.x * currentZoom,
                    mouseY = pt.y * currentZoom

                evt.preventDefault()

                if (rightClick) {
                    currentMode = 'select'
                    lastClickPoint = pt
                }

                // This would seem to be unnecessary...
                // if (!['select', 'resize'].includes(currentMode)) {
                //   setGradient();
                // }

                let x = mouseX / currentZoom,
                    y = mouseY / currentZoom
                let mouseTarget = getMouseTarget(evt)

                if (
                    mouseTarget.tagName === 'a' &&
                    mouseTarget.childNodes.length === 1
                ) {
                    mouseTarget = mouseTarget.firstChild
                }

                // realX/y ignores grid-snap value
                const realX = x
                rStartX = startX = x
                const realY = y
                rStartY = startY = y

                if (curConfig.gridSnapping) {
                    x = snapToGrid(x)
                    y = snapToGrid(y)
                    startX = snapToGrid(startX)
                    startY = snapToGrid(startY)
                }

                // if it is a selector grip, then it must be a single element selected,
                // set the mouseTarget to that and update the mode to rotate/resize

                if (
                    mouseTarget === selectorManager.selectorParentGroup &&
                    !isNullish(selectedElements[0])
                ) {
                    const grip = evt.target
                    const griptype = elData(grip, 'type')
                    // rotating
                    if (griptype === 'rotate') {
                        currentMode = 'rotate'
                        // resizing
                    } else if (griptype === 'resize') {
                        currentMode = 'resize'
                        currentResizeMode = elData(grip, 'dir')
                    }
                    mouseTarget = selectedElements[0]
                }

                startTransform = mouseTarget.getAttribute('transform')

                const tlist = getTransformList(mouseTarget)
                switch (currentMode) {
                    case 'select':
                        started = true
                        currentResizeMode = 'none'
                        if (rightClick) {
                            started = false
                        }

                        if (mouseTarget !== svgroot) {
                            // if this element is not yet selected, clear selection and select it
                            if (!selectedElements.includes(mouseTarget)) {
                                // only clear selection if shift is not pressed (otherwise, add
                                // element to selection)
                                if (!evt.shiftKey) {
                                    // No need to do the call here as it will be done on addToSelection
                                    clearSelection(true)
                                }
                                addToSelection([mouseTarget])
                                justSelected = mouseTarget
                                pathActions.clear()
                            }
                            // else if it's a path, go into pathedit mode in mouseup

                            if (!rightClick) {
                                // insert a dummy transform so if the element(s) are moved it will have
                                // a transform to use for its translate
                                for (const selectedElement of selectedElements) {
                                    if (isNullish(selectedElement)) {
                                        continue
                                    }
                                    const slist = getTransformList(
                                        selectedElement
                                    )
                                    if (slist.numberOfItems) {
                                        slist.insertItemBefore(
                                            svgroot.createSVGTransform(),
                                            0
                                        )
                                    } else {
                                        slist.appendItem(
                                            svgroot.createSVGTransform()
                                        )
                                    }
                                }
                            }
                        } else if (!rightClick) {
                            clearSelection()
                            currentMode = 'multiselect'
                            if (isNullish(rubberBox)) {
                                rubberBox = selectorManager.getRubberBandBox()
                            }
                            rStartX *= currentZoom
                            rStartY *= currentZoom
                            // console.log('p',[evt.pageX, evt.pageY]);
                            // console.log('c',[evt.clientX, evt.clientY]);
                            // console.log('o',[evt.offsetX, evt.offsetY]);
                            // console.log('s',[startX, startY]);

                            assignAttributes(
                                rubberBox,
                                {
                                    x: rStartX,
                                    y: rStartY,
                                    width: 0,
                                    height: 0,
                                    display: 'inline'
                                },
                                100
                            )
                        }
                        break
                    case 'zoom':
                        started = true
                        if (isNullish(rubberBox)) {
                            rubberBox = selectorManager.getRubberBandBox()
                        }
                        assignAttributes(
                            rubberBox,
                            {
                                x: realX * currentZoom,
                                y: realX * currentZoom,
                                width: 0,
                                height: 0,
                                display: 'inline'
                            },
                            100
                        )
                        break
                    case 'resize': {
                        started = true
                        startX = x
                        startY = y

                        // Getting the BBox from the selection box, since we know we
                        // want to orient around it
                        initBbox = utilsGetBBox($('#selectedBox0')[0])
                        const bb = {}
                        $.each(initBbox, function(key, val) {
                            bb[key] = val / currentZoom
                        })
                        initBbox = bb

                        // append three dummy transforms to the tlist so that
                        // we can translate,scale,translate in mousemove
                        const pos = getRotationAngle(mouseTarget) ? 1 : 0

                        if (hasMatrixTransform(tlist)) {
                            tlist.insertItemBefore(
                                svgroot.createSVGTransform(),
                                pos
                            )
                            tlist.insertItemBefore(
                                svgroot.createSVGTransform(),
                                pos
                            )
                            tlist.insertItemBefore(
                                svgroot.createSVGTransform(),
                                pos
                            )
                        } else {
                            tlist.appendItem(svgroot.createSVGTransform())
                            tlist.appendItem(svgroot.createSVGTransform())
                            tlist.appendItem(svgroot.createSVGTransform())

                            if (supportsNonScalingStroke()) {
                                // Handle crash for newer Chrome and Safari 6 (Mobile and Desktop):
                                // https://code.google.com/p/svg-edit/issues/detail?id=904
                                // Chromium issue: https://code.google.com/p/chromium/issues/detail?id=114625
                                // TODO: Remove this workaround once vendor fixes the issue
                                const iswebkit = isWebkit()

                                let delayedStroke
                                if (iswebkit) {
                                    delayedStroke = function(ele) {
                                        const stroke_ = ele.getAttribute(
                                            'stroke'
                                        )
                                        ele.removeAttribute('stroke')
                                        // Re-apply stroke after delay. Anything higher than 1 seems to cause flicker
                                        if (stroke_ !== null)
                                            setTimeout(function() {
                                                ele.setAttribute(
                                                    'stroke',
                                                    stroke_
                                                )
                                            }, 0)
                                    }
                                }
                                mouseTarget.style.vectorEffect =
                                    'non-scaling-stroke'
                                if (iswebkit) {
                                    delayedStroke(mouseTarget)
                                }

                                const all = mouseTarget.getElementsByTagName(
                                        '*'
                                    ),
                                    len = all.length
                                for (let i = 0; i < len; i++) {
                                    if (!all[i].style) {
                                        // mathML
                                        continue
                                    }
                                    all[i].style.vectorEffect =
                                        'non-scaling-stroke'
                                    if (iswebkit) {
                                        delayedStroke(all[i])
                                    }
                                }
                            }
                        }
                        break
                    }
                    case 'fhellipse':
                    case 'fhrect':
                    case 'fhpath':
                        start.x = realX
                        start.y = realY
                        started = true
                        dAttr = realX + ',' + realY + ' '
                        // Commented out as doing nothing now:
                        // strokeW = parseFloat(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;
                        addSVGElementFromJson({
                            element: 'polyline',
                            curStyles: true,
                            attr: {
                                points: dAttr,
                                id: getNextId(),
                                fill: 'none',
                                opacity: curShape.opacity / 2,
                                'stroke-linecap': 'round',
                                style: 'pointer-events:none'
                            }
                        })
                        freehand.minx = realX
                        freehand.maxx = realX
                        freehand.miny = realY
                        freehand.maxy = realY
                        break
                    case 'image': {
                        started = true
                        const newImage = addSVGElementFromJson({
                            element: 'image',
                            attr: {
                                x: x >= 0 ? x : 10,
                                y: y >= 0 ? y : 10,
                                width: 0,
                                height: 0,
                                id: getNextId(),
                                opacity: curShape.opacity / 2,
                                style: 'pointer-events:inherit'
                            }
                        })
                        setHref(
                            newImage,
                            lastGoodImgUrl === 'images/logo.png'
                                ? 'images/image-default.png'
                                : lastGoodImgUrl
                        )
                        preventClickDefault(newImage)
                        break
                    }
                    case 'square':
                    // FIXME: once we create the rect, we lose information that this was a square
                    // (for resizing purposes this could be important)
                    // Fallthrough
                    case 'rect':
                        started = true
                        startX = x
                        startY = y
                        addSVGElementFromJson({
                            element: 'rect',
                            curStyles: true,
                            attr: {
                                x,
                                y,
                                width: 0,
                                height: 0,
                                id: getNextId(),
                                opacity: curShape.opacity / 2
                            }
                        })
                        break
                    case 'line': {
                        started = true
                        const strokeW =
                            Number(curShape.stroke_width) === 0
                                ? 1
                                : curShape.stroke_width
                        addSVGElementFromJson({
                            element: 'line',
                            curStyles: true,
                            attr: {
                                x1: x,
                                y1: y,
                                x2: x,
                                y2: y,
                                id: getNextId(),
                                stroke: curShape.stroke,
                                'stroke-width': strokeW,
                                'stroke-dasharray': curShape.stroke_dasharray,
                                'stroke-linejoin': curShape.stroke_linejoin,
                                'stroke-linecap': curShape.stroke_linecap,
                                'stroke-opacity': curShape.stroke_opacity,
                                fill: 'none',
                                opacity: curShape.opacity / 2,
                                style: 'pointer-events:none'
                            }
                        })
                        break
                    }
                    case 'circle':
                        started = true
                        addSVGElementFromJson({
                            element: 'circle',
                            curStyles: true,
                            attr: {
                                cx: x,
                                cy: y,
                                r: 0,
                                id: getNextId(),
                                opacity: curShape.opacity / 2
                            }
                        })
                        break
                    case 'ellipse':
                        started = true
                        addSVGElementFromJson({
                            element: 'ellipse',
                            curStyles: true,
                            attr: {
                                cx: x,
                                cy: y,
                                rx: 0,
                                ry: 0,
                                id: getNextId(),
                                opacity: curShape.opacity / 2
                            }
                        })
                        break
                    case 'text':
                        started = true
                        /* const newText = */ addSVGElementFromJson({
                            element: 'text',
                            curStyles: true,
                            attr: {
                                x,
                                y,
                                id: getNextId(),
                                fill: curText.fill,
                                'stroke-width': curText.stroke_width,
                                'font-size': curText.font_size,
                                'font-family': curText.font_family,
                                'text-anchor': 'middle',
                                'xml:space': 'preserve',
                                opacity: curShape.opacity
                            }
                        })
                        // newText.textContent = 'text';
                        break
                    case 'path':
                    // Fall through
                    case 'pathedit':
                        startX *= currentZoom
                        startY *= currentZoom
                        pathActions.mouseDown(
                            evt,
                            mouseTarget,
                            startX,
                            startY,
                            drawAttrs
                        )
                        started = true
                        break
                    case 'textedit':
                        startX *= currentZoom
                        startY *= currentZoom
                        textActions.mouseDown(evt, mouseTarget, startX, startY)
                        started = true
                        break
                    case 'rotate':
                        started = true
                        // we are starting an undoable change (a drag-rotation)
                        canvas.undoMgr.beginUndoableChange(
                            'transform',
                            selectedElements
                        )
                        break
                    default:
                        // This could occur in an extension
                        break
                }

                /**
                 * The main (left) mouse button is held down on the canvas area
                 * @event module:svgcanvas.SvgCanvas#event:ext_mouseDown
                 * @type {PlainObject}
                 * @property {MouseEvent} event The event object
                 * @property {Float} start_x x coordinate on canvas
                 * @property {Float} start_y y coordinate on canvas
                 * @property {Element[]} selectedElements An array of the selected Elements
                 */
                const extResult = runExtensions(
                    'mouseDown',
                    /** @type {module:svgcanvas.SvgCanvas#event:ext_mouseDown} */ {
                        event: evt,
                        start_x: startX,
                        start_y: startY,
                        selectedElements
                    },
                    true
                )

                $.each(extResult, function(i, r) {
                    if (r && r.started) {
                        started = true
                    }
                })
            }

            // in this function we do not record any state changes yet (but we do update
            // any elements that are still being created, moved or resized on the canvas)
            /**
             *
             * @param {MouseEvent} evt
             * @fires module:svgcanvas.SvgCanvas#event:transition
             * @fires module:svgcanvas.SvgCanvas#event:ext_mouseMove
             * @returns {void}
             */
            const mouseMove = function(evt) {
                if (!started) {
                    return
                }
                if (evt.button === 1 || canvas.spaceKey) {
                    return
                }

                let i,
                    xya,
                    c,
                    cx,
                    cy,
                    dx,
                    dy,
                    len,
                    angle,
                    box,
                    selected = selectedElements[0]
                const pt = transformPoint(evt.pageX, evt.pageY, rootSctm),
                    mouseX = pt.x * currentZoom,
                    mouseY = pt.y * currentZoom,
                    shape = getElem(getId())

                let realX = mouseX / currentZoom
                let x = realX
                let realY = mouseY / currentZoom
                let y = realY

                if (curConfig.gridSnapping) {
                    x = snapToGrid(x)
                    y = snapToGrid(y)
                }

                evt.preventDefault()
                let tlist
                switch (currentMode) {
                    case 'select': {
                        // we temporarily use a translate on the element(s) being dragged
                        // this transform is removed upon mousing up and the element is
                        // relocated to the new location
                        if (selectedElements[0] !== null) {
                            // 如果存在父级id,标签为text且只选择了一个元素
                            let room_parent_id = $(selectedElements[0]).attr(
                                'room_parent_id'
                            )
                            if (
                                room_parent_id &&
                                selectedElements.length === 1
                            ) {
                                let nodeName = $('#' + room_parent_id)[0]
                                    .nodeName
                                let parentElem = $('#' + room_parent_id)
                                let parentElem_x
                                let parentElem_y
                                let parentElem_w
                                let parentElem_h

                                switch (nodeName) {
                                    case 'rect':
                                        parentElem_x = parentElem.attr('x')
                                        parentElem_y = parentElem.attr('y')
                                        parentElem_w = parentElem.attr('width')
                                        parentElem_h = parentElem.attr('height')

                                        break

                                    case 'path':
                                        let nodeInfo = document
                                            .getElementById(room_parent_id)
                                            .getBBox()
                                        parentElem_x = nodeInfo.x
                                        parentElem_y = nodeInfo.y
                                        parentElem_w = nodeInfo.width
                                        parentElem_h = nodeInfo.height

                                        break
                                }

                                // x轴最大最小距离确定
                                if (x <= parentElem_x) {
                                    x = parentElem_x
                                }

                                if (x >= parentElem_x + parentElem_w) {
                                    x = parentElem_x + parentElem_w
                                }

                                // y轴最大最小距离确定
                                if (y <= parentElem_y) {
                                    y = parentElem_y
                                }
                                if (y >= parentElem_y + parentElem_h) {
                                    y = parentElem_y + parentElem_h
                                }
                            }

                            dx = x - startX
                            dy = y - startY

                            if (curConfig.gridSnapping) {
                                dx = snapToGrid(dx)
                                dy = snapToGrid(dy)
                            }

                            /*
                            // Commenting out as currently has no effect
                            if (evt.shiftKey) {
                                xya = snapToAngle(startX, startY, x, y);
                                ({x, y} = xya);
                            }
                            */

                            {
                                // 关联房源和文本，拖拽房源时也一同拖拽文本
                                let l = selectedElements.length
                                let res = selectedElements.find(
                                    obj => obj.attributes['room_parent_id']
                                )
                                if (!res) {
                                    for (i = 0; i < l; ++i) {
                                        selected = selectedElements[i]
                                        let room_text_id = $(selected).attr(
                                            'room_text_id'
                                        )
                                        if (room_text_id) {
                                            let textElem = $(
                                                '#' + room_text_id
                                            )[0]
                                            selectedElements.push(textElem)
                                            // addToSelection([textElem])
                                            // clearSelection(true)
                                        }
                                    }
                                }
                            }
                            if (dx !== 0 || dy !== 0) {
                                len = selectedElements.length
                                for (i = 0; i < len; ++i) {
                                    selected = selectedElements[i]
                                    if (isNullish(selected)) {
                                        break
                                    }
                                    // if (i === 0) {
                                    //   const box = utilsGetBBox(selected);
                                    //     selectedBBoxes[i].x = box.x + dx;
                                    //     selectedBBoxes[i].y = box.y + dy;
                                    // }

                                    // update the dummy transform in our transform list
                                    // to be a translate
                                    const xform = svgroot.createSVGTransform()
                                    tlist = getTransformList(selected)
                                    // Note that if Webkit and there's no ID for this
                                    // element, the dummy transform may have gotten lost.
                                    // This results in unexpected behaviour

                                    xform.setTranslate(dx, dy)
                                    if (tlist.numberOfItems) {
                                        tlist.replaceItem(xform, 0)
                                    } else {
                                        tlist.appendItem(xform)
                                    }
                                    // update our internal bbox that we're tracking while dragging
                                    selectorManager
                                        .requestSelector(selected)
                                        .resize()
                                }

                                call('transition', selectedElements)
                            }
                        }
                        break
                    }
                    case 'multiselect': {
                        realX *= currentZoom
                        realY *= currentZoom
                        assignAttributes(
                            rubberBox,
                            {
                                x: Math.min(rStartX, realX),
                                y: Math.min(rStartY, realY),
                                width: Math.abs(realX - rStartX),
                                height: Math.abs(realY - rStartY)
                            },
                            100
                        )

                        // for each selected:
                        // - if newList contains selected, do nothing
                        // - if newList doesn't contain selected, remove it from selected
                        // - for any newList that was not in selectedElements, add it to selected
                        const elemsToRemove = selectedElements.slice(),
                            elemsToAdd = [],
                            newList = getIntersectionList()

                        // For every element in the intersection, add if not present in selectedElements.
                        len = newList.length
                        for (i = 0; i < len; ++i) {
                            const intElem = newList[i]
                            // Found an element that was not selected before, so we should add it.
                            if (!selectedElements.includes(intElem)) {
                                elemsToAdd.push(intElem)
                            }
                            // Found an element that was already selected, so we shouldn't remove it.
                            const foundInd = elemsToRemove.indexOf(intElem)
                            if (foundInd !== -1) {
                                elemsToRemove.splice(foundInd, 1)
                            }
                        }

                        if (elemsToRemove.length > 0) {
                            canvas.removeFromSelection(elemsToRemove)
                        }

                        if (elemsToAdd.length > 0) {
                            canvas.addToSelection(elemsToAdd)
                        }

                        break
                    }
                    case 'resize': {
                        // we track the resize bounding box and translate/scale the selected element
                        // while the mouse is down, when mouse goes up, we use this to recalculate
                        // the shape's coordinates
                        tlist = getTransformList(selected)
                        const hasMatrix = hasMatrixTransform(tlist)
                        box = hasMatrix ? initBbox : utilsGetBBox(selected)
                        let left = box.x,
                            top = box.y,
                            { width, height } = box
                        dx = x - startX
                        dy = y - startY

                        if (curConfig.gridSnapping) {
                            dx = snapToGrid(dx)
                            dy = snapToGrid(dy)
                            height = snapToGrid(height)
                            width = snapToGrid(width)
                        }

                        // if rotated, adjust the dx,dy values
                        angle = getRotationAngle(selected)
                        if (angle) {
                            const r = Math.sqrt(dx * dx + dy * dy),
                                theta =
                                    Math.atan2(dy, dx) -
                                    (angle * Math.PI) / 180.0
                            dx = r * Math.cos(theta)
                            dy = r * Math.sin(theta)
                        }

                        // if not stretching in y direction, set dy to 0
                        // if not stretching in x direction, set dx to 0
                        if (
                            !currentResizeMode.includes('n') &&
                            !currentResizeMode.includes('s')
                        ) {
                            dy = 0
                        }
                        if (
                            !currentResizeMode.includes('e') &&
                            !currentResizeMode.includes('w')
                        ) {
                            dx = 0
                        }

                        let // ts = null,
                            tx = 0,
                            ty = 0,
                            sy = height ? (height + dy) / height : 1,
                            sx = width ? (width + dx) / width : 1
                        // if we are dragging on the north side, then adjust the scale factor and ty
                        if (currentResizeMode.includes('n')) {
                            sy = height ? (height - dy) / height : 1
                            ty = height
                        }

                        // if we dragging on the east side, then adjust the scale factor and tx
                        if (currentResizeMode.includes('w')) {
                            sx = width ? (width - dx) / width : 1
                            tx = width
                        }

                        // update the transform list with translate,scale,translate
                        const translateOrigin = svgroot.createSVGTransform(),
                            scale = svgroot.createSVGTransform(),
                            translateBack = svgroot.createSVGTransform()

                        if (curConfig.gridSnapping) {
                            left = snapToGrid(left)
                            tx = snapToGrid(tx)
                            top = snapToGrid(top)
                            ty = snapToGrid(ty)
                        }

                        translateOrigin.setTranslate(-(left + tx), -(top + ty))
                        if (evt.shiftKey) {
                            if (sx === 1) {
                                sx = sy
                            } else {
                                sy = sx
                            }
                        }
                        scale.setScale(sx, sy)

                        translateBack.setTranslate(left + tx, top + ty)
                        if (hasMatrix) {
                            const diff = angle ? 1 : 0
                            tlist.replaceItem(translateOrigin, 2 + diff)
                            tlist.replaceItem(scale, 1 + diff)
                            tlist.replaceItem(translateBack, Number(diff))
                        } else {
                            const N = tlist.numberOfItems
                            tlist.replaceItem(translateBack, N - 3)
                            tlist.replaceItem(scale, N - 2)
                            tlist.replaceItem(translateOrigin, N - 1)
                        }

                        selectorManager.requestSelector(selected).resize()

                        call('transition', selectedElements)

                        break
                    }
                    case 'zoom': {
                        realX *= currentZoom
                        realY *= currentZoom
                        assignAttributes(
                            rubberBox,
                            {
                                x: Math.min(rStartX * currentZoom, realX),
                                y: Math.min(rStartY * currentZoom, realY),
                                width: Math.abs(realX - rStartX * currentZoom),
                                height: Math.abs(realY - rStartY * currentZoom)
                            },
                            100
                        )
                        break
                    }
                    case 'text': {
                        assignAttributes(
                            shape,
                            {
                                x,
                                y
                            },
                            1000
                        )
                        break
                    }
                    case 'line': {
                        if (curConfig.gridSnapping) {
                            x = snapToGrid(x)
                            y = snapToGrid(y)
                        }

                        let x2 = x
                        let y2 = y

                        if (evt.shiftKey) {
                            xya = snapToAngle(startX, startY, x2, y2)
                            x2 = xya.x
                            y2 = xya.y
                        }

                        shape.setAttribute('x2', x2)
                        shape.setAttribute('y2', y2)
                        break
                    }
                    case 'foreignObject':
                    // fall through
                    case 'square':
                    // fall through
                    case 'rect':
                    // fall through
                    case 'image': {
                        const square = currentMode === 'square' || evt.shiftKey
                        let w = Math.abs(x - startX),
                            h = Math.abs(y - startY)
                        let newX, newY
                        if (square) {
                            w = h = Math.max(w, h)
                            newX = startX < x ? startX : startX - w
                            newY = startY < y ? startY : startY - h
                        } else {
                            newX = Math.min(startX, x)
                            newY = Math.min(startY, y)
                        }

                        if (curConfig.gridSnapping) {
                            w = snapToGrid(w)
                            h = snapToGrid(h)
                            newX = snapToGrid(newX)
                            newY = snapToGrid(newY)
                        }

                        assignAttributes(
                            shape,
                            {
                                width: w,
                                height: h,
                                x: newX,
                                y: newY
                            },
                            1000
                        )

                        break
                    }
                    case 'circle': {
                        c = $(shape).attr(['cx', 'cy'])
                        ;({ cx, cy } = c)
                        let rad = Math.sqrt(
                            (x - cx) * (x - cx) + (y - cy) * (y - cy)
                        )
                        if (curConfig.gridSnapping) {
                            rad = snapToGrid(rad)
                        }
                        shape.setAttribute('r', rad)
                        break
                    }
                    case 'ellipse': {
                        c = $(shape).attr(['cx', 'cy'])
                        ;({ cx, cy } = c)
                        if (curConfig.gridSnapping) {
                            x = snapToGrid(x)
                            cx = snapToGrid(cx)
                            y = snapToGrid(y)
                            cy = snapToGrid(cy)
                        }
                        shape.setAttribute('rx', Math.abs(x - cx))
                        const ry = Math.abs(evt.shiftKey ? x - cx : y - cy)
                        shape.setAttribute('ry', ry)
                        break
                    }
                    case 'fhellipse':
                    case 'fhrect': {
                        freehand.minx = Math.min(realX, freehand.minx)
                        freehand.maxx = Math.max(realX, freehand.maxx)
                        freehand.miny = Math.min(realY, freehand.miny)
                        freehand.maxy = Math.max(realY, freehand.maxy)
                    }
                    // Fallthrough
                    case 'fhpath': {
                        // dAttr += + realX + ',' + realY + ' ';
                        // shape.setAttribute('points', dAttr);
                        end.x = realX
                        end.y = realY
                        if (controllPoint2.x && controllPoint2.y) {
                            for (i = 0; i < STEP_COUNT - 1; i++) {
                                parameter = i / STEP_COUNT
                                nextParameter = (i + 1) / STEP_COUNT
                                bSpline = getBsplinePoint(nextParameter)
                                nextPos = bSpline
                                bSpline = getBsplinePoint(parameter)
                                sumDistance += Math.sqrt(
                                    (nextPos.x - bSpline.x) *
                                        (nextPos.x - bSpline.x) +
                                        (nextPos.y - bSpline.y) *
                                            (nextPos.y - bSpline.y)
                                )
                                if (sumDistance > THRESHOLD_DIST) {
                                    sumDistance -= THRESHOLD_DIST

                                    // Faster than completely re-writing the points attribute.
                                    const point = svgcontent.createSVGPoint()
                                    point.x = bSpline.x
                                    point.y = bSpline.y
                                    shape.points.appendItem(point)
                                }
                            }
                        }
                        controllPoint2 = {
                            x: controllPoint1.x,
                            y: controllPoint1.y
                        }
                        controllPoint1 = { x: start.x, y: start.y }
                        start = { x: end.x, y: end.y }
                        break
                        // update path stretch line coordinates
                    }
                    case 'path':
                    // fall through
                    case 'pathedit': {
                        x *= currentZoom
                        y *= currentZoom

                        if (curConfig.gridSnapping) {
                            x = snapToGrid(x)
                            y = snapToGrid(y)
                            startX = snapToGrid(startX)
                            startY = snapToGrid(startY)
                        }
                        if (evt.shiftKey) {
                            const { path } = pathModule
                            let x1, y1
                            if (path) {
                                x1 = path.dragging ? path.dragging[0] : startX
                                y1 = path.dragging ? path.dragging[1] : startY
                            } else {
                                x1 = startX
                                y1 = startY
                            }
                            xya = snapToAngle(x1, y1, x, y)
                            ;({ x, y } = xya)
                        }

                        if (
                            rubberBox &&
                            rubberBox.getAttribute('display') !== 'none'
                        ) {
                            realX *= currentZoom
                            realY *= currentZoom
                            assignAttributes(
                                rubberBox,
                                {
                                    x: Math.min(rStartX * currentZoom, realX),
                                    y: Math.min(rStartY * currentZoom, realY),
                                    width: Math.abs(
                                        realX - rStartX * currentZoom
                                    ),
                                    height: Math.abs(
                                        realY - rStartY * currentZoom
                                    )
                                },
                                100
                            )
                        }
                        pathActions.mouseMove(x, y)

                        break
                    }
                    case 'textedit': {
                        x *= currentZoom
                        y *= currentZoom
                        // if (rubberBox && rubberBox.getAttribute('display') !== 'none') {
                        //   assignAttributes(rubberBox, {
                        //     x: Math.min(startX, x),
                        //     y: Math.min(startY, y),
                        //     width: Math.abs(x - startX),
                        //     height: Math.abs(y - startY)
                        //   }, 100);
                        // }

                        textActions.mouseMove(mouseX, mouseY)

                        break
                    }
                    case 'rotate': {
                        box = utilsGetBBox(selected)
                        cx = box.x + box.width / 2
                        cy = box.y + box.height / 2
                        const m = getMatrix(selected),
                            center = transformPoint(cx, cy, m)
                        cx = center.x
                        cy = center.y
                        angle =
                            (Math.atan2(cy - y, cx - x) * (180 / Math.PI) -
                                90) %
                            360
                        if (curConfig.gridSnapping) {
                            angle = snapToGrid(angle)
                        }
                        if (evt.shiftKey) {
                            // restrict rotations to nice angles (WRS)
                            const snap = 45
                            angle = Math.round(angle / snap) * snap
                        }

                        canvas.setRotationAngle(
                            angle < -180 ? 360 + angle : angle,
                            true
                        )
                        call('transition', selectedElements)
                        break
                    }
                    default:
                        break
                }

                /**
                 * The mouse has moved on the canvas area
                 * @event module:svgcanvas.SvgCanvas#event:ext_mouseMove
                 * @type {PlainObject}
                 * @property {MouseEvent} event The event object
                 * @property {Float} mouse_x x coordinate on canvas
                 * @property {Float} mouse_y y coordinate on canvas
                 * @property {Element} selected Refers to the first selected element
                 */
                runExtensions(
                    'mouseMove',
                    /** @type {module:svgcanvas.SvgCanvas#event:ext_mouseMove} */ {
                        event: evt,
                        mouse_x: mouseX,
                        mouse_y: mouseY,
                        selected
                    }
                )
            } // mouseMove()

            // - in create mode, the element's opacity is set properly, we create an InsertElementCommand
            // and store it on the Undo stack
            // - in move/resize mode, the element's attributes which were affected by the move/resize are
            // identified, a ChangeElementCommand is created and stored on the stack for those attrs
            // this is done in when we recalculate the selected dimensions()
            /**
             *
             * @param {MouseEvent} evt
             * @fires module:svgcanvas.SvgCanvas#event:zoomed
             * @fires module:svgcanvas.SvgCanvas#event:changed
             * @fires module:svgcanvas.SvgCanvas#event:ext_mouseUp
             * @returns {void}
             */
            const mouseUp = function(evt) {
                if (evt.button === 2) {
                    return
                }
                const tempJustSelected = justSelected
                justSelected = null
                if (!started) {
                    return
                }
                const pt = transformPoint(evt.pageX, evt.pageY, rootSctm),
                    mouseX = pt.x * currentZoom,
                    mouseY = pt.y * currentZoom,
                    x = mouseX / currentZoom,
                    y = mouseY / currentZoom

                let element = getElem(getId())
                let keep = false

                const realX = x
                const realY = y

                // TODO: Make true when in multi-unit mode
                const useUnit = false // (curConfig.baseUnit !== 'px');
                started = false
                let attrs, t
                switch (currentMode) {
                    // intentionally fall-through to select here
                    case 'resize':
                    case 'multiselect':
                        if (!isNullish(rubberBox)) {
                            rubberBox.setAttribute('display', 'none')
                            curBBoxes = []
                        }
                        currentMode = 'select'
                    // Fallthrough
                    case 'select':
                        if (!isNullish(selectedElements[0])) {
                            // if we only have one selected element
                            if (isNullish(selectedElements[1])) {
                                // set our current stroke/fill properties to the element's
                                const selected = selectedElements[0]
                                switch (selected.tagName) {
                                    case 'g':
                                    case 'use':
                                    case 'image':
                                    case 'foreignObject':
                                        break
                                    default:
                                        curProperties.fill = selected.getAttribute(
                                            'fill'
                                        )
                                        curProperties.fill_opacity = selected.getAttribute(
                                            'fill-opacity'
                                        )
                                        curProperties.stroke = selected.getAttribute(
                                            'stroke'
                                        )
                                        curProperties.stroke_opacity = selected.getAttribute(
                                            'stroke-opacity'
                                        )
                                        curProperties.stroke_width = selected.getAttribute(
                                            'stroke-width'
                                        )
                                        curProperties.stroke_dasharray = selected.getAttribute(
                                            'stroke-dasharray'
                                        )
                                        curProperties.stroke_linejoin = selected.getAttribute(
                                            'stroke-linejoin'
                                        )
                                        curProperties.stroke_linecap = selected.getAttribute(
                                            'stroke-linecap'
                                        )
                                }

                                if (selected.tagName === 'text') {
                                    curText.font_size = selected.getAttribute(
                                        'font-size'
                                    )
                                    curText.font_family = selected.getAttribute(
                                        'font-family'
                                    )
                                }
                                selectorManager
                                    .requestSelector(selected)
                                    .showGrips(true)

                                // This shouldn't be necessary as it was done on mouseDown...
                                // call('selected', [selected]);
                            }
                            // always recalculate dimensions to strip off stray identity transforms
                            recalculateAllSelectedDimensions()
                            // if it was being dragged/resized
                            if (realX !== rStartX || realY !== rStartY) {
                                const len = selectedElements.length
                                for (let i = 0; i < len; ++i) {
                                    if (isNullish(selectedElements[i])) {
                                        break
                                    }
                                    if (!selectedElements[i].firstChild) {
                                        // Not needed for groups (incorrectly resizes elems), possibly not needed at all?
                                        selectorManager
                                            .requestSelector(
                                                selectedElements[i]
                                            )
                                            .resize()
                                    }
                                }
                                // no change in position/size, so maybe we should move to pathedit
                            } else {
                                t = evt.target
                                if (
                                    selectedElements[0].nodeName === 'path' &&
                                    isNullish(selectedElements[1])
                                ) {
                                    pathActions.select(selectedElements[0])
                                    // if it was a path
                                    // else, if it was selected and this is a shift-click, remove it from selection
                                } else if (evt.shiftKey) {
                                    if (tempJustSelected !== t) {
                                        canvas.removeFromSelection([t])
                                    }
                                }
                            } // no change in mouse position

                            // Remove non-scaling stroke
                            if (supportsNonScalingStroke()) {
                                const elem = selectedElements[0]
                                if (elem) {
                                    elem.removeAttribute('style')
                                    walkTree(elem, function(el) {
                                        el.removeAttribute('style')
                                    })
                                }
                            }
                        }
                        return
                    case 'zoom': {
                        if (!isNullish(rubberBox)) {
                            rubberBox.setAttribute('display', 'none')
                        }
                        const factor = evt.shiftKey ? 0.5 : 2
                        call('zoomed', {
                            x: Math.min(rStartX, realX),
                            y: Math.min(rStartY, realY),
                            width: Math.abs(realX - rStartX),
                            height: Math.abs(realY - rStartY),
                            factor
                        })
                        return
                    }
                    case 'fhpath': {
                        // Check that the path contains at least 2 points; a degenerate one-point path
                        // causes problems.
                        // Webkit ignores how we set the points attribute with commas and uses space
                        // to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870
                        sumDistance = 0
                        controllPoint2 = { x: 0, y: 0 }
                        controllPoint1 = { x: 0, y: 0 }
                        start = { x: 0, y: 0 }
                        end = { x: 0, y: 0 }
                        const coords = element.getAttribute('points')
                        const commaIndex = coords.indexOf(',')
                        if (commaIndex >= 0) {
                            keep = coords.includes(',', commaIndex + 1)
                        } else {
                            keep = coords.includes(' ', coords.indexOf(' ') + 1)
                        }
                        if (keep) {
                            element = pathActions.smoothPolylineIntoPath(
                                element
                            )
                        }
                        break
                    }
                    case 'line':
                        attrs = $(element).attr(['x1', 'x2', 'y1', 'y2'])
                        keep = attrs.x1 !== attrs.x2 || attrs.y1 !== attrs.y2
                        break
                    case 'foreignObject':
                    case 'square':
                    case 'rect':
                    case 'image':
                        attrs = $(element).attr(['width', 'height'])
                        // Image should be kept regardless of size (use inherit dimensions later)
                        keep =
                            attrs.width ||
                            attrs.height ||
                            currentMode === 'image'
                        break
                    case 'circle':
                        keep = element.getAttribute('r') !== '0'
                        break
                    case 'ellipse':
                        attrs = $(element).attr(['rx', 'ry'])
                        keep = attrs.rx || attrs.ry
                        break
                    case 'fhellipse':
                        if (
                            freehand.maxx - freehand.minx > 0 &&
                            freehand.maxy - freehand.miny > 0
                        ) {
                            element = addSVGElementFromJson({
                                element: 'ellipse',
                                curStyles: true,
                                attr: {
                                    cx: (freehand.minx + freehand.maxx) / 2,
                                    cy: (freehand.miny + freehand.maxy) / 2,
                                    rx: (freehand.maxx - freehand.minx) / 2,
                                    ry: (freehand.maxy - freehand.miny) / 2,
                                    id: getId()
                                }
                            })
                            call('changed', [element])
                            keep = true
                        }
                        break
                    case 'fhrect':
                        if (
                            freehand.maxx - freehand.minx > 0 &&
                            freehand.maxy - freehand.miny > 0
                        ) {
                            element = addSVGElementFromJson({
                                element: 'rect',
                                curStyles: true,
                                attr: {
                                    x: freehand.minx,
                                    y: freehand.miny,
                                    width: freehand.maxx - freehand.minx,
                                    height: freehand.maxy - freehand.miny,
                                    id: getId()
                                }
                            })
                            call('changed', [element])
                            keep = true
                        }
                        break
                    case 'text':
                        keep = true
                        selectOnly([element])
                        textActions.start(element)
                        break
                    case 'path': {
                        // set element to null here so that it is not removed nor finalized
                        element = null
                        // continue to be set to true so that mouseMove happens
                        started = true

                        const res = pathActions.mouseUp(
                            evt,
                            element,
                            mouseX,
                            mouseY
                        )
                        ;({ element } = res)
                        ;({ keep } = res)
                        break
                    }
                    case 'pathedit':
                        keep = true
                        element = null
                        pathActions.mouseUp(evt)
                        break
                    case 'textedit':
                        keep = false
                        element = null
                        textActions.mouseUp(evt, mouseX, mouseY)
                        break
                    case 'rotate': {
                        keep = true
                        element = null
                        currentMode = 'select'
                        const batchCmd = canvas.undoMgr.finishUndoableChange()
                        if (!batchCmd.isEmpty()) {
                            addCommandToHistory(batchCmd)
                        }
                        // perform recalculation to weed out any stray identity transforms that might get stuck
                        recalculateAllSelectedDimensions()
                        call('changed', selectedElements)
                        break
                    }
                    default:
                        // This could occur in an extension
                        break
                }

                /**
                 * The main (left) mouse button is released (anywhere)
                 * @event module:svgcanvas.SvgCanvas#event:ext_mouseUp
                 * @type {PlainObject}
                 * @property {MouseEvent} event The event object
                 * @property {Float} mouse_x x coordinate on canvas
                 * @property {Float} mouse_y y coordinate on canvas
                 */
                const extResult = runExtensions(
                    'mouseUp',
                    /** @type {module:svgcanvas.SvgCanvas#event:ext_mouseUp} */ {
                        event: evt,
                        mouse_x: mouseX,
                        mouse_y: mouseY
                    },
                    true
                )

                $.each(extResult, function(i, r) {
                    if (r) {
                        keep = r.keep || keep
                        ;({ element } = r)
                        started = r.started || started
                    }
                })

                if (!keep && !isNullish(element)) {
                    getCurrentDrawing().releaseId(getId())
                    element.remove()
                    element = null

                    t = evt.target

                    // if this element is in a group, go up until we reach the top-level group
                    // just below the layer groups
                    // TODO: once we implement links, we also would have to check for <a> elements
                    while (
                        t &&
                        t.parentNode &&
                        t.parentNode.parentNode &&
                        t.parentNode.parentNode.tagName === 'g'
                    ) {
                        t = t.parentNode
                    }
                    // if we are not in the middle of creating a path, and we've clicked on some shape,
                    // then go to Select mode.
                    // WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg>
                    if (
                        (currentMode !== 'path' || !drawnPath) &&
                        t &&
                        t.parentNode &&
                        t.parentNode.id !== 'selectorParentGroup' &&
                        t.id !== 'svgcanvas' &&
                        t.id !== 'svgroot'
                    ) {
                        // switch into "select" mode if we've clicked on an element
                        canvas.setMode('select')
                        selectOnly([t], true)
                    }
                } else if (!isNullish(element)) {
                    /**
                     * @name module:svgcanvas.SvgCanvas#addedNew
                     * @type {boolean}
                     */
                    canvas.addedNew = true

                    if (useUnit) {
                        convertAttrs(element)
                    }

                    let aniDur = 0.2
                    let cAni
                    if (
                        opacAni.beginElement &&
                        parseFloat(element.getAttribute('opacity')) !==
                            curShape.opacity
                    ) {
                        cAni = $(opacAni)
                            .clone()
                            .attr({
                                to: curShape.opacity,
                                dur: aniDur
                            })
                            .appendTo(element)
                        try {
                            // Fails in FF4 on foreignObject
                            cAni[0].beginElement()
                        } catch (e) {}
                    } else {
                        aniDur = 0
                    }

                    // Ideally this would be done on the endEvent of the animation,
                    // but that doesn't seem to be supported in Webkit
                    setTimeout(function() {
                        if (cAni) {
                            cAni.remove()
                        }
                        element.setAttribute('opacity', curShape.opacity)
                        element.setAttribute('style', 'pointer-events:inherit')
                        cleanupElement(element)
                        if (currentMode === 'path') {
                            pathActions.toEditMode(element)
                        } else if (curConfig.selectNew) {
                            selectOnly([element], true)
                        }
                        // we create the insert command that is stored on the stack
                        // undo means to call cmd.unapply(), redo means to call cmd.apply()
                        addCommandToHistory(new InsertElementCommand(element))

                        call('changed', [element])
                    }, aniDur * 1000)
                }

                startTransform = null
            }

            const dblClick = function(evt) {
                const evtTarget = evt.target
                // 有disabled属性直接return
                if ($(evtTarget).attr('disabled')) return

                const parent = evtTarget.parentNode

                // Do nothing if already in current group
                if (parent === currentGroup) {
                    return
                }

                let mouseTarget = getMouseTarget(evt)
                const { tagName } = mouseTarget

                if (tagName === 'text' && currentMode !== 'textedit') {
                    const pt = transformPoint(evt.pageX, evt.pageY, rootSctm)
                    textActions.select(mouseTarget, pt.x, pt.y)
                }

                if (
                    (tagName === 'g' || tagName === 'a') &&
                    getRotationAngle(mouseTarget)
                ) {
                    // TODO: Allow method of in-group editing without having to do
                    // this (similar to editing rotated paths)

                    // Ungroup and regroup
                    pushGroupProperties(mouseTarget)
                    mouseTarget = selectedElements[0]
                    clearSelection(true)
                }
                // Reset context
                if (currentGroup) {
                    draw.leaveContext()
                }

                if (
                    (parent.tagName !== 'g' && parent.tagName !== 'a') ||
                    parent === getCurrentDrawing().getCurrentLayer() ||
                    mouseTarget === selectorManager.selectorParentGroup
                ) {
                    // Escape from in-group edit
                    return
                }
                draw.setContext(mouseTarget)
            }

            // prevent links from being followed in the canvas
            const handleLinkInCanvas = function(e) {
                e.preventDefault()
                return false
            }

            // Added mouseup to the container here.
            // TODO(codedread): Figure out why after the Closure compiler, the window mouseup is ignored.
            $(container)
                .mousedown(mouseDown)
                .mousemove(mouseMove)
                .click(handleLinkInCanvas)
                .dblclick(dblClick)
                .mouseup(mouseUp)
            // $(window).mouseup(mouseUp);

            // TODO(rafaelcastrocouto): User preference for shift key and zoom factor
            $(container).bind(
                'mousewheel DOMMouseScroll',
                /**
                 * @param {Event} e
                 * @fires module:svgcanvas.SvgCanvas#event:updateCanvas
                 * @fires module:svgcanvas.SvgCanvas#event:zoomDone
                 * @returns {void}
                 */
                function(e) {
                    if (!e.shiftKey) {
                        return
                    }

                    e.preventDefault()
                    const evt = e.originalEvent

                    rootSctm = $('#svgcontent g')[0]
                        .getScreenCTM()
                        .inverse()

                    const workarea = $('#workarea')
                    const scrbar = 15
                    const rulerwidth = curConfig.showRulers ? 16 : 0

                    // mouse relative to content area in content pixels
                    const pt = transformPoint(evt.pageX, evt.pageY, rootSctm)

                    // full work area width in screen pixels
                    const editorFullW = workarea.width()
                    const editorFullH = workarea.height()

                    // work area width minus scroll and ruler in screen pixels
                    const editorW = editorFullW - scrbar - rulerwidth
                    const editorH = editorFullH - scrbar - rulerwidth

                    // work area width in content pixels
                    const workareaViewW = editorW * rootSctm.a
                    const workareaViewH = editorH * rootSctm.d

                    // content offset from canvas in screen pixels
                    const wOffset = workarea.offset()
                    const wOffsetLeft = wOffset.left + rulerwidth
                    const wOffsetTop = wOffset.top + rulerwidth

                    const delta = evt.wheelDelta
                        ? evt.wheelDelta
                        : evt.detail
                        ? -evt.detail
                        : 0
                    if (!delta) {
                        return
                    }

                    let factor = Math.max(3 / 4, Math.min(4 / 3, delta))

                    let wZoom, hZoom
                    if (factor > 1) {
                        wZoom =
                            Math.ceil(
                                (editorW / workareaViewW) * factor * 100
                            ) / 100
                        hZoom =
                            Math.ceil(
                                (editorH / workareaViewH) * factor * 100
                            ) / 100
                    } else {
                        wZoom =
                            Math.floor(
                                (editorW / workareaViewW) * factor * 100
                            ) / 100
                        hZoom =
                            Math.floor(
                                (editorH / workareaViewH) * factor * 100
                            ) / 100
                    }
                    let zoomlevel = Math.min(wZoom, hZoom)
                    zoomlevel = Math.min(10, Math.max(0.01, zoomlevel))
                    if (zoomlevel === currentZoom) {
                        return
                    }
                    factor = zoomlevel / currentZoom

                    // top left of workarea in content pixels before zoom
                    const topLeftOld = transformPoint(
                        wOffsetLeft,
                        wOffsetTop,
                        rootSctm
                    )

                    // top left of workarea in content pixels after zoom
                    const topLeftNew = {
                        x: pt.x - (pt.x - topLeftOld.x) / factor,
                        y: pt.y - (pt.y - topLeftOld.y) / factor
                    }

                    // top left of workarea in canvas pixels relative to content after zoom
                    const topLeftNewCanvas = {
                        x: topLeftNew.x * zoomlevel,
                        y: topLeftNew.y * zoomlevel
                    }

                    // new center in canvas pixels
                    const newCtr = {
                        x: topLeftNewCanvas.x - rulerwidth + editorFullW / 2,
                        y: topLeftNewCanvas.y - rulerwidth + editorFullH / 2
                    }

                    canvas.setZoom(zoomlevel)
                    $('#zoom').val((zoomlevel * 100).toFixed(1))

                    call('updateCanvas', { center: false, newCtr })
                    call('zoomDone')
                }
            )
        })()

        /**
         * Group: Text edit functions
         * Functions relating to editing text elements
         * @namespace {PlainObject} textActions
         * @memberof module:svgcanvas.SvgCanvas#
         */
        const textActions = (canvas.textActions = (function() {
            let curtext
            let textinput
            let cursor
            let selblock
            let blinker
            let chardata = []
            let textbb // , transbb;
            let matrix
            let lastX, lastY
            let allowDbl

            /**
             *
             * @param {Integer} index
             * @returns {void}
             */
            function setCursor(index) {
                if (!textinput) return
                const empty = textinput.value === ''
                $(textinput).focus()

                if (!arguments.length) {
                    if (empty) {
                        index = 0
                    } else {
                        if (
                            textinput.selectionEnd !== textinput.selectionStart
                        ) {
                            return
                        }
                        index = textinput.selectionEnd
                    }
                }

                const charbb = chardata[index]
                if (!empty) {
                    textinput.setSelectionRange(index, index)
                }
                cursor = getElem('text_cursor')
                if (!cursor) {
                    cursor = document.createElementNS(NS.SVG, 'line')
                    assignAttributes(cursor, {
                        id: 'text_cursor',
                        stroke: '#333',
                        'stroke-width': 1
                    })
                    cursor = getElem('selectorParentGroup').appendChild(cursor)
                }

                if (!blinker) {
                    blinker = setInterval(function() {
                        const show = cursor.getAttribute('display') === 'none'
                        cursor.setAttribute('display', show ? 'inline' : 'none')
                    }, 600)
                }

                const startPt = ptToScreen(charbb.x, textbb.y)
                const endPt = ptToScreen(charbb.x, textbb.y + textbb.height)

                assignAttributes(cursor, {
                    x1: startPt.x,
                    y1: startPt.y,
                    x2: endPt.x,
                    y2: endPt.y,
                    visibility: 'visible',
                    display: 'inline'
                })

                if (selblock) {
                    selblock.setAttribute('d', '')
                }
            }

            /**
             *
             * @param {Integer} start
             * @param {Integer} end
             * @param {boolean} skipInput
             * @returns {void}
             */
            function setSelection(start, end, skipInput) {
                if (start === end) {
                    setCursor(end)
                    return
                }

                if (!skipInput) {
                    textinput.setSelectionRange(start, end)
                }

                selblock = getElem('text_selectblock')
                if (!selblock) {
                    selblock = document.createElementNS(NS.SVG, 'path')
                    assignAttributes(selblock, {
                        id: 'text_selectblock',
                        fill: 'green',
                        opacity: 0.5,
                        style: 'pointer-events:none'
                    })
                    getElem('selectorParentGroup').append(selblock)
                }

                const startbb = chardata[start]
                const endbb = chardata[end]

                cursor.setAttribute('visibility', 'hidden')

                const tl = ptToScreen(startbb.x, textbb.y),
                    tr = ptToScreen(
                        startbb.x + (endbb.x - startbb.x),
                        textbb.y
                    ),
                    bl = ptToScreen(startbb.x, textbb.y + textbb.height),
                    br = ptToScreen(
                        startbb.x + (endbb.x - startbb.x),
                        textbb.y + textbb.height
                    )

                const dstr =
                    'M' +
                    tl.x +
                    ',' +
                    tl.y +
                    ' L' +
                    tr.x +
                    ',' +
                    tr.y +
                    ' ' +
                    br.x +
                    ',' +
                    br.y +
                    ' ' +
                    bl.x +
                    ',' +
                    bl.y +
                    'z'

                assignAttributes(selblock, {
                    d: dstr,
                    display: 'inline'
                })
            }

            /**
             *
             * @param {Float} mouseX
             * @param {Float} mouseY
             * @returns {Integer}
             */
            function getIndexFromPoint(mouseX, mouseY) {
                // Position cursor here
                const pt = svgroot.createSVGPoint()
                pt.x = mouseX
                pt.y = mouseY

                // No content, so return 0
                if (chardata.length === 1 || chardata.length === 0) {
                    return 0
                }
                // Determine if cursor should be on left or right of character
                let charpos = curtext.getCharNumAtPosition(pt)
                if (charpos < 0) {
                    // Out of text range, look at mouse coords
                    charpos = chardata.length - 2
                    if (mouseX <= chardata[0].x) {
                        charpos = 0
                    }
                } else if (charpos >= chardata.length - 2) {
                    charpos = chardata.length - 2
                }
                const charbb = chardata[charpos]
                const mid = charbb.x + charbb.width / 2
                if (mouseX > mid) {
                    charpos++
                }
                return charpos
            }

            /**
             *
             * @param {Float} mouseX
             * @param {Float} mouseY
             * @returns {void}
             */
            function setCursorFromPoint(mouseX, mouseY) {
                setCursor(getIndexFromPoint(mouseX, mouseY))
            }

            /**
             *
             * @param {Float} x
             * @param {Float} y
             * @param {boolean} apply
             * @returns {void}
             */
            function setEndSelectionFromPoint(x, y, apply) {
                if (!textinput) return
                const i1 = textinput.selectionStart
                const i2 = getIndexFromPoint(x, y)

                const start = Math.min(i1, i2)
                const end = Math.max(i1, i2)
                setSelection(start, end, !apply)
            }

            /**
             *
             * @param {Float} xIn
             * @param {Float} yIn
             * @returns {module:math.XYObject}
             */
            function screenToPt(xIn, yIn) {
                const out = {
                    x: xIn,
                    y: yIn
                }

                out.x /= currentZoom
                out.y /= currentZoom

                if (matrix) {
                    const pt = transformPoint(out.x, out.y, matrix.inverse())
                    out.x = pt.x
                    out.y = pt.y
                }

                return out
            }

            /**
             *
             * @param {Float} xIn
             * @param {Float} yIn
             * @returns {module:math.XYObject}
             */
            function ptToScreen(xIn, yIn) {
                const out = {
                    x: xIn,
                    y: yIn
                }

                if (matrix) {
                    const pt = transformPoint(out.x, out.y, matrix)
                    out.x = pt.x
                    out.y = pt.y
                }

                out.x *= currentZoom
                out.y *= currentZoom

                return out
            }

            /*
// Not currently in use
function hideCursor () {
  if (cursor) {
    cursor.setAttribute('visibility', 'hidden');
  }
}
*/

            /**
             *
             * @param {Event} evt
             * @returns {void}
             */
            function selectAll(evt) {
                setSelection(0, curtext.textContent.length)
                $(this).unbind(evt)
            }

            /**
             *
             * @param {Event} evt
             * @returns {void}
             */
            function selectWord(evt) {
                if (!allowDbl || !curtext) {
                    return
                }

                const ept = transformPoint(evt.pageX, evt.pageY, rootSctm),
                    mouseX = ept.x * currentZoom,
                    mouseY = ept.y * currentZoom
                const pt = screenToPt(mouseX, mouseY)

                const index = getIndexFromPoint(pt.x, pt.y)
                const str = curtext.textContent
                const first = str.substr(0, index).replace(/[a-z0-9]+$/i, '')
                    .length
                const m = str.substr(index).match(/^[a-z0-9]+/i)
                const last = (m ? m[0].length : 0) + index
                setSelection(first, last)

                // Set tripleclick
                $(evt.target).click(selectAll)
                setTimeout(function() {
                    $(evt.target).unbind('click', selectAll)
                }, 300)
            }

            return /** @lends module:svgcanvas.SvgCanvas#textActions */ {
                /**
                 * @param {Element} target
                 * @param {Float} x
                 * @param {Float} y
                 * @returns {void}
                 */
                select(target, x, y) {
                    curtext = target
                    textActions.toEditMode(x, y)
                },
                /**
                 * @param {Element} elem
                 * @returns {void}
                 */
                start(elem) {
                    curtext = elem
                    textActions.toEditMode()
                },
                /**
                 * @param {external:MouseEvent} evt
                 * @param {Element} mouseTarget
                 * @param {Float} startX
                 * @param {Float} startY
                 * @returns {void}
                 */
                mouseDown(evt, mouseTarget, startX, startY) {
                    if (!textinput) return
                    const pt = screenToPt(startX, startY)

                    textinput.focus()
                    setCursorFromPoint(pt.x, pt.y)
                    lastX = startX
                    lastY = startY

                    // TODO: Find way to block native selection
                },
                /**
                 * @param {Float} mouseX
                 * @param {Float} mouseY
                 * @returns {void}
                 */
                mouseMove(mouseX, mouseY) {
                    const pt = screenToPt(mouseX, mouseY)
                    setEndSelectionFromPoint(pt.x, pt.y)
                },
                /**
                 * @param {external:MouseEvent} evt
                 * @param {Float} mouseX
                 * @param {Float} mouseY
                 * @returns {void}
                 */
                mouseUp(evt, mouseX, mouseY) {
                    const pt = screenToPt(mouseX, mouseY)

                    setEndSelectionFromPoint(pt.x, pt.y, true)

                    // TODO: Find a way to make this work: Use transformed BBox instead of evt.target
                    // if (lastX === mouseX && lastY === mouseY
                    //   && !rectsIntersect(transbb, {x: pt.x, y: pt.y, width: 0, height: 0})) {
                    //   textActions.toSelectMode(true);
                    // }

                    if (
                        evt.target !== curtext &&
                        mouseX < lastX + 2 &&
                        mouseX > lastX - 2 &&
                        mouseY < lastY + 2 &&
                        mouseY > lastY - 2
                    ) {
                        textActions.toSelectMode(true)
                    }
                },
                /**
                 * @function
                 * @param {Integer} index
                 * @returns {void}
                 */
                setCursor,
                /**
                 * @param {Float} x
                 * @param {Float} y
                 * @returns {void}
                 */
                toEditMode(x, y) {
                    allowDbl = false
                    currentMode = 'textedit'
                    selectorManager.requestSelector(curtext).showGrips(false)
                    // Make selector group accept clicks
                    /* const selector = */ selectorManager.requestSelector(
                        curtext
                    ) // Do we need this? Has side effect of setting lock, so keeping for now, but next line wasn't being used
                    // const sel = selector.selectorRect;

                    textActions.init()

                    $(curtext).css('cursor', 'text')

                    // if (supportsEditableText()) {
                    //   curtext.setAttribute('editable', 'simple');
                    //   return;
                    // }

                    if (!arguments.length) {
                        setCursor()
                    } else {
                        const pt = screenToPt(x, y)
                        setCursorFromPoint(pt.x, pt.y)
                    }

                    setTimeout(function() {
                        allowDbl = true
                    }, 300)
                },
                /**
                 * @param {boolean|Element} selectElem
                 * @fires module:svgcanvas.SvgCanvas#event:selected
                 * @returns {void}
                 */
                toSelectMode(selectElem) {
                    currentMode = 'select'
                    clearInterval(blinker)
                    blinker = null
                    if (selblock) {
                        $(selblock).attr('display', 'none')
                    }
                    if (cursor) {
                        $(cursor).attr('visibility', 'hidden')
                    }
                    $(curtext).css('cursor', 'move')

                    if (selectElem) {
                        clearSelection()
                        $(curtext).css('cursor', 'move')

                        call('selected', [curtext])
                        addToSelection([curtext], true)
                    }
                    if (curtext && !curtext.textContent.length) {
                        // No content, so delete
                        canvas.deleteSelectedElements()
                    }

                    $(textinput).blur()

                    curtext = false

                    // if (supportsEditableText()) {
                    //   curtext.removeAttribute('editable');
                    // }
                },
                /**
                 * @param {Element} elem
                 * @returns {void}
                 */
                setInputElem(elem) {
                    textinput = elem
                    // $(textinput).blur(hideCursor);
                },
                /**
                 * @returns {void}
                 */
                clear() {
                    if (currentMode === 'textedit') {
                        textActions.toSelectMode()
                    }
                },
                /**
                 * @param {Element} inputElem Not in use
                 * @returns {void}
                 */
                init(inputElem) {
                    if (!curtext || !textinput) {
                        return
                    }
                    let i, end
                    // if (supportsEditableText()) {
                    //   curtext.select();
                    //   return;
                    // }

                    if (!curtext.parentNode) {
                        // Result of the ffClone, need to get correct element
                        curtext = selectedElements[0]
                        selectorManager
                            .requestSelector(curtext)
                            .showGrips(false)
                    }

                    const str = curtext.textContent
                    const len = str.length

                    const xform = curtext.getAttribute('transform')

                    textbb = utilsGetBBox(curtext)

                    matrix = xform ? getMatrix(curtext) : null

                    chardata = []
                    chardata.length = len
                    textinput.focus()

                    $(curtext)
                        .unbind('dblclick', selectWord)
                        .dblclick(selectWord)

                    if (!len) {
                        end = { x: textbb.x + textbb.width / 2, width: 0 }
                    }

                    for (i = 0; i < len; i++) {
                        const start = curtext.getStartPositionOfChar(i)
                        end = curtext.getEndPositionOfChar(i)

                        if (!supportsGoodTextCharPos()) {
                            const offset = canvas.contentW * currentZoom
                            start.x -= offset
                            end.x -= offset

                            start.x /= currentZoom
                            end.x /= currentZoom
                        }

                        // Get a "bbox" equivalent for each character. Uses the
                        // bbox data of the actual text for y, height purposes

                        // TODO: Decide if y, width and height are actually necessary
                        chardata[i] = {
                            x: start.x,
                            y: textbb.y, // start.y?
                            width: end.x - start.x,
                            height: textbb.height
                        }
                    }

                    // Add a last bbox for cursor at end of text
                    chardata.push({
                        x: end.x,
                        width: 0
                    })
                    setSelection(
                        textinput.selectionStart,
                        textinput.selectionEnd,
                        true
                    )
                }
            }
        })())

        /**
         * Group: Serialization
         */

        /**
         * Looks at DOM elements inside the `<defs>` to see if they are referred to,
         * removes them from the DOM if they are not.
         * @function module:svgcanvas.SvgCanvas#removeUnusedDefElems
         * @returns {Integer} The number of elements that were removed
         */
        const removeUnusedDefElems = (this.removeUnusedDefElems = function() {
            const defs = svgcontent.getElementsByTagNameNS(NS.SVG, 'defs')
            if (!defs || !defs.length) {
                return 0
            }

            // if (!defs.firstChild) { return; }

            const defelemUses = []
            let numRemoved = 0
            const attrs = [
                'fill',
                'stroke',
                'filter',
                'marker-start',
                'marker-mid',
                'marker-end'
            ]
            const alen = attrs.length

            const allEls = svgcontent.getElementsByTagNameNS(NS.SVG, '*')
            const allLen = allEls.length

            let i, j
            for (i = 0; i < allLen; i++) {
                const el = allEls[i]
                for (j = 0; j < alen; j++) {
                    const ref = getUrlFromAttr(el.getAttribute(attrs[j]))
                    if (ref) {
                        defelemUses.push(ref.substr(1))
                    }
                }

                // gradients can refer to other gradients
                const href = getHref(el)
                if (href && href.startsWith('#')) {
                    defelemUses.push(href.substr(1))
                }
            }

            const defelems = $(defs).find(
                'linearGradient, radialGradient, filter, marker, svg, symbol'
            )
            i = defelems.length
            while (i--) {
                const defelem = defelems[i]
                const { id } = defelem
                if (!defelemUses.includes(id)) {
                    // Not found, so remove (but remember)
                    removedElements[id] = defelem
                    defelem.remove()
                    numRemoved++
                }
            }

            return numRemoved
        })

        /**
         * Main function to set up the SVG content for output.
         * @function module:svgcanvas.SvgCanvas#svgCanvasToString
         * @returns {string} The SVG image for output
         */
        this.svgCanvasToString = function() {
            // keep calling it until there are none to remove
            while (removeUnusedDefElems() > 0) {} // eslint-disable-line no-empty

            pathActions.clear(true)

            // Keep SVG-Edit comment on top
            $.each(svgcontent.childNodes, function(i, node) {
                if (
                    i &&
                    node.nodeType === 8 &&
                    node.data.includes('Created with')
                ) {
                    svgcontent.firstChild.before(node)
                }
            })

            // Move out of in-group editing mode
            if (currentGroup) {
                draw.leaveContext()
                selectOnly([currentGroup])
            }

            const nakedSvgs = []

            // Unwrap gsvg if it has no special attributes (only id and style)
            $(svgcontent)
                .find('g:data(gsvg)')
                .each(function() {
                    const attrs = this.attributes
                    let len = attrs.length
                    for (let i = 0; i < len; i++) {
                        if (
                            attrs[i].nodeName === 'id' ||
                            attrs[i].nodeName === 'style'
                        ) {
                            len--
                        }
                    }
                    // No significant attributes, so ungroup
                    if (len <= 0) {
                        const svg = this.firstChild
                        nakedSvgs.push(svg)
                        $(this).replaceWith(svg)
                    }
                })
            const output = this.svgToString(svgcontent, 0)

            // Rewrap gsvg
            if (nakedSvgs.length) {
                $(nakedSvgs).each(function() {
                    groupSvgElem(this)
                })
            }

            return output
        }

        /**
         * Sub function ran on each SVG element to convert it to a string as desired.
         * @function module:svgcanvas.SvgCanvas#svgToString
         * @param {Element} elem - The SVG element to convert
         * @param {Integer} indent - Number of spaces to indent this tag
         * @returns {string} The given element as an SVG tag
         */
        this.svgToString = function(elem, indent) {
            const out = []
            const unit = curConfig.baseUnit
            const unitRe = new RegExp('^-?[\\d\\.]+' + unit + '$')

            if (elem) {
                cleanupElement(elem)
                const attrs = [...elem.attributes]
                const childs = elem.childNodes
                attrs.sort((a, b) => {
                    return a.name > b.name ? -1 : 1
                })

                for (let i = 0; i < indent; i++) {
                    out.push(' ')
                }
                out.push('<')
                out.push(elem.nodeName)
                if (elem.id === 'svgcontent') {
                    // Process root element separately
                    const res = getResolution()

                    const vb = ''
                    // TODO: Allow this by dividing all values by current baseVal
                    // Note that this also means we should properly deal with this on import
                    // if (curConfig.baseUnit !== 'px') {
                    //   const unit = curConfig.baseUnit;
                    //   const unitM = getTypeMap()[unit];
                    //   res.w = shortFloat(res.w / unitM);
                    //   res.h = shortFloat(res.h / unitM);
                    //   vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"';
                    //   res.w += unit;
                    //   res.h += unit;
                    // }

                    if (unit !== 'px') {
                        res.w = convertUnit(res.w, unit) + unit
                        res.h = convertUnit(res.h, unit) + unit
                    }

                    out.push(
                        ' width="' +
                            res.w +
                            '" height="' +
                            res.h +
                            '"' +
                            vb +
                            ' xmlns="' +
                            NS.SVG +
                            '"'
                    )

                    const nsuris = {}

                    // Check elements for namespaces, add if found
                    $(elem)
                        .find('*')
                        .andSelf()
                        .each(function() {
                            // const el = this;
                            // for some elements have no attribute
                            const uri = this.namespaceURI
                            if (
                                uri &&
                                !nsuris[uri] &&
                                nsMap[uri] &&
                                nsMap[uri] !== 'xmlns' &&
                                nsMap[uri] !== 'xml'
                            ) {
                                nsuris[uri] = true
                                out.push(
                                    ' xmlns:' + nsMap[uri] + '="' + uri + '"'
                                )
                            }

                            $.each(this.attributes, function(i, attr) {
                                const u = attr.namespaceURI
                                if (
                                    u &&
                                    !nsuris[u] &&
                                    nsMap[u] !== 'xmlns' &&
                                    nsMap[u] !== 'xml'
                                ) {
                                    nsuris[u] = true
                                    out.push(
                                        ' xmlns:' + nsMap[u] + '="' + u + '"'
                                    )
                                }
                            })
                        })

                    let i = attrs.length
                    const attrNames = [
                        'width',
                        'height',
                        'xmlns',
                        'x',
                        'y',
                        'viewBox',
                        'id',
                        'overflow'
                    ]
                    while (i--) {
                        const attr = attrs[i]
                        const attrVal = toXml(attr.value)

                        // Namespaces have already been dealt with, so skip
                        if (attr.nodeName.startsWith('xmlns:')) {
                            continue
                        }

                        // only serialize attributes we don't use internally
                        if (
                            attrVal !== '' &&
                            !attrNames.includes(attr.localName)
                        ) {
                            if (
                                !attr.namespaceURI ||
                                nsMap[attr.namespaceURI]
                            ) {
                                out.push(' ')
                                out.push(attr.nodeName)
                                out.push('="')
                                out.push(attrVal)
                                out.push('"')
                            }
                        }
                    }
                } else {
                    // Skip empty defs
                    if (elem.nodeName === 'defs' && !elem.firstChild) {
                        return ''
                    }

                    const mozAttrs = [
                        '-moz-math-font-style',
                        '_moz-math-font-style'
                    ]
                    for (let i = attrs.length - 1; i >= 0; i--) {
                        const attr = attrs[i]
                        let attrVal = toXml(attr.value)
                        // remove bogus attributes added by Gecko
                        if (mozAttrs.includes(attr.localName)) {
                            continue
                        }
                        if (attrVal !== '') {
                            if (attrVal.startsWith('pointer-events')) {
                                continue
                            }
                            if (
                                attr.localName === 'class' &&
                                attrVal.startsWith('se_')
                            ) {
                                continue
                            }
                            out.push(' ')
                            if (attr.localName === 'd') {
                                attrVal = pathActions.convertPath(elem, true)
                            }
                            if (!isNaN(attrVal)) {
                                // attrVal = shortFloat(attrVal)
                                attrVal = attrVal
                            } else if (unitRe.test(attrVal)) {
                                // attrVal = shortFloat(attrVal) + unit
                                attrVal = attrVal + unit
                            }

                            // Embed images when saving
                            if (
                                saveOptions.apply &&
                                elem.nodeName === 'image' &&
                                attr.localName === 'href' &&
                                saveOptions.images &&
                                saveOptions.images === 'embed'
                            ) {
                                const img = encodableImages[attrVal]
                                if (img) {
                                    attrVal = img
                                }
                            }

                            // map various namespaces to our fixed namespace prefixes
                            // (the default xmlns attribute itself does not get a prefix)
                            if (
                                !attr.namespaceURI ||
                                attr.namespaceURI === NS.SVG ||
                                nsMap[attr.namespaceURI]
                            ) {
                                out.push(attr.nodeName)
                                out.push('="')
                                out.push(attrVal)
                                out.push('"')
                            }
                        }
                    }
                }

                if (elem.hasChildNodes()) {
                    out.push('>')
                    indent++
                    let bOneLine = false

                    for (let i = 0; i < childs.length; i++) {
                        const child = childs.item(i)
                        switch (child.nodeType) {
                            case 1: // element node
                                out.push('\n')
                                out.push(
                                    this.svgToString(childs.item(i), indent)
                                )
                                break
                            case 3: {
                                // text node
                                const str = child.nodeValue.replace(
                                    /^\s+|\s+$/g,
                                    ''
                                )
                                if (str !== '') {
                                    bOneLine = true
                                    out.push(String(toXml(str)))
                                }
                                break
                            }
                            case 4: // cdata node
                                out.push('\n')
                                out.push(new Array(indent + 1).join(' '))
                                out.push('<![CDATA[')
                                out.push(child.nodeValue)
                                out.push(']]>')
                                break
                            case 8: // comment
                                out.push('\n')
                                out.push(new Array(indent + 1).join(' '))
                                out.push('<!--')
                                out.push(child.data)
                                out.push('-->')
                                break
                        } // switch on node type
                    }
                    indent--
                    if (!bOneLine) {
                        out.push('\n')
                        for (let i = 0; i < indent; i++) {
                            out.push(' ')
                        }
                    }
                    out.push('</')
                    out.push(elem.nodeName)
                    out.push('>')
                } else {
                    out.push('/>')
                }
            }
            return out.join('')
        } // end svgToString()

        /**
         * Function to run when image data is found
         * @callback module:svgcanvas.ImageEmbeddedCallback
         * @param {string|false} result Data URL
         * @returns {void}
         */
        /**
         * Converts a given image file to a data URL when possible, then runs a given callback.
         * @function module:svgcanvas.SvgCanvas#embedImage
         * @param {string} src - The path/URL of the image
         * @returns {Promise<string|false>} Resolves to a Data URL (string|false)
         */
        this.embedImage = function(src) {
            // Todo: Remove this Promise in favor of making an async/await `Image.load` utility
            return new Promise(function(resolve, reject) {
                // eslint-disable-line promise/avoid-new
                // load in the image and once it's loaded, get the dimensions
                $(new Image())
                    .load(function(response, status, xhr) {
                        if (status === 'error') {
                            reject(
                                new Error(
                                    'Error loading image: ' +
                                        xhr.status +
                                        ' ' +
                                        xhr.statusText
                                )
                            )
                            return
                        }
                        // create a canvas the same size as the raster image
                        const cvs = document.createElement('canvas')
                        cvs.width = this.width
                        cvs.height = this.height
                        // load the raster image into the canvas
                        cvs.getContext('2d').drawImage(this, 0, 0)
                        // retrieve the data: URL
                        try {
                            let urldata =
                                ';svgedit_url=' + encodeURIComponent(src)
                            urldata = cvs
                                .toDataURL()
                                .replace(';base64', urldata + ';base64')
                            encodableImages[src] = urldata
                        } catch (e) {
                            encodableImages[src] = false
                        }
                        lastGoodImgUrl = src
                        resolve(encodableImages[src])
                    })
                    .attr('src', src)
            })
        }

        /**
         * Sets a given URL to be a "last good image" URL.
         * @function module:svgcanvas.SvgCanvas#setGoodImage
         * @param {string} val
         * @returns {void}
         */
        this.setGoodImage = function(val) {
            lastGoodImgUrl = val
        }

        /**
         * Does nothing by default, handled by optional widget/extension.
         * @function module:svgcanvas.SvgCanvas#open
         * @returns {void}
         */
        this.open = function() {
            /* */
        }

        /**
         * Serializes the current drawing into SVG XML text and passes it to the 'saved' handler.
         * This function also includes the XML prolog. Clients of the `SvgCanvas` bind their save
         * function to the 'saved' event.
         * @function module:svgcanvas.SvgCanvas#save
         * @param {module:svgcanvas.SaveOptions} opts
         * @fires module:svgcanvas.SvgCanvas#event:saved
         * @returns {void}
         */
        this.save = function(opts) {
            // remove the selected outline before serializing
            clearSelection()
            // Update save options if provided
            if (opts) {
                $.extend(saveOptions, opts)
            }
            saveOptions.apply = true

            // no need for doctype, see https://jwatt.org/svg/authoring/#doctype-declaration
            const str = this.svgCanvasToString()
            call('saved', str)
        }

        /**
         * @typedef {PlainObject} module:svgcanvas.IssuesAndCodes
         * @property {string[]} issueCodes The locale-independent code names
         * @property {string[]} issues The localized descriptions
         */

        /**
         * Codes only is useful for locale-independent detection.
         * @returns {module:svgcanvas.IssuesAndCodes}
         */
        function getIssues() {
            // remove the selected outline before serializing
            clearSelection()

            // Check for known CanVG issues
            const issues = []
            const issueCodes = []

            // Selector and notice
            const issueList = {
                feGaussianBlur: uiStrings.exportNoBlur,
                foreignObject: uiStrings.exportNoforeignObject,
                '[stroke-dasharray]': uiStrings.exportNoDashArray
            }
            const content = $(svgcontent)

            // Add font/text check if Canvas Text API is not implemented
            if (!('font' in $('<canvas>')[0].getContext('2d'))) {
                issueList.text = uiStrings.exportNoText
            }

            $.each(issueList, function(sel, descr) {
                if (content.find(sel).length) {
                    issueCodes.push(sel)
                    issues.push(descr)
                }
            })
            return { issues, issueCodes }
        }

        let canvg
        /**
         * @typedef {"feGaussianBlur"|"foreignObject"|"[stroke-dasharray]"|"text"} module:svgcanvas.IssueCode
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.ImageExportedResults
         * @property {string} datauri Contents as a Data URL
         * @property {string} bloburl May be the empty string
         * @property {string} svg The SVG contents as a string
         * @property {string[]} issues The localization messages of `issueCodes`
         * @property {module:svgcanvas.IssueCode[]} issueCodes CanVG issues found with the SVG
         * @property {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} type The chosen image type
         * @property {"image/png"|"image/jpeg"|"image/bmp"|"image/webp"} mimeType The image MIME type
         * @property {Float} quality A decimal between 0 and 1 (for use with JPEG or WEBP)
         * @property {string} exportWindowName A convenience for passing along a `window.name` to target a window on which the export could be added
         */

        /**
         * Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image,
         * then calls "exported" with an object including the string, image
         * information, and any issues found.
         * @function module:svgcanvas.SvgCanvas#rasterExport
         * @param {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} [imgType="PNG"]
         * @param {Float} [quality] Between 0 and 1
         * @param {string} [exportWindowName]
         * @param {PlainObject} [opts]
         * @param {boolean} [opts.avoidEvent]
         * @fires module:svgcanvas.SvgCanvas#event:exported
         * @todo Confirm/fix ICO type
         * @returns {Promise<module:svgcanvas.ImageExportedResults>} Resolves to {@link module:svgcanvas.ImageExportedResults}
         */
        this.rasterExport = async function(
            imgType,
            quality,
            exportWindowName,
            opts = {}
        ) {
            const type = imgType === 'ICO' ? 'BMP' : imgType || 'PNG'
            const mimeType = 'image/' + type.toLowerCase()
            const { issues, issueCodes } = getIssues()
            const svg = this.svgCanvasToString()

            if (!canvg) {
                // eslint-disable-next-line require-atomic-updates
                ;({ canvg } = await importSetGlobal(
                    curConfig.canvgPath + 'canvg.js',
                    {
                        global: 'canvg'
                    }
                ))
            }
            if (!$('#export_canvas').length) {
                $('<canvas>', { id: 'export_canvas' })
                    .hide()
                    .appendTo('body')
            }
            const c = $('#export_canvas')[0]
            c.width = canvas.contentW
            c.height = canvas.contentH

            await canvg(c, svg)
            // Todo: Make async/await utility in place of `toBlob`, so we can remove this constructor
            return new Promise((resolve, reject) => {
                // eslint-disable-line promise/avoid-new
                const dataURLType = type.toLowerCase()
                const datauri = quality
                    ? c.toDataURL('image/' + dataURLType, quality)
                    : c.toDataURL('image/' + dataURLType)
                let bloburl
                /**
                 * Called when `bloburl` is available for export.
                 * @returns {void}
                 */
                function done() {
                    const obj = {
                        datauri,
                        bloburl,
                        svg,
                        issues,
                        issueCodes,
                        type: imgType,
                        mimeType,
                        quality,
                        exportWindowName
                    }
                    if (!opts.avoidEvent) {
                        call('exported', obj)
                    }
                    resolve(obj)
                }
                if (c.toBlob) {
                    c.toBlob(
                        blob => {
                            bloburl = createObjectURL(blob)
                            done()
                        },
                        mimeType,
                        quality
                    )
                    return
                }
                bloburl = dataURLToObjectURL(datauri)
                done()
            })
        }
        /**
         * @external jsPDF
         */
        /**
         * @typedef {void|"save"|"arraybuffer"|"blob"|"datauristring"|"dataurlstring"|"dataurlnewwindow"|"datauri"|"dataurl"} external:jsPDF.OutputType
         * @todo Newer version to add also allows these `outputType` values "bloburi"|"bloburl" which return strings, so document here and for `outputType` of `module:svgcanvas.PDFExportedResults` below if added
         */
        /**
         * @typedef {PlainObject} module:svgcanvas.PDFExportedResults
         * @property {string} svg The SVG PDF output
         * @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`;
         * if `undefined`, "datauristring", "dataurlstring", "datauri",
         * or "dataurl", will be a string (`undefined` gives a document, while the others
         * build as Data URLs; "datauri" and "dataurl" change the location of the current page); if
         * "arraybuffer", will return `ArrayBuffer`; if "blob", returns a `Blob`;
         * if "dataurlnewwindow", will change the current page's location and return a string
         * if in Safari and no window object is found; otherwise opens in, and returns, a new `window`
         * object; if "save", will have the same return as "dataurlnewwindow" if
         * `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise
         * returns `undefined` but attempts to save
         * @property {external:jsPDF.OutputType} outputType
         * @property {string[]} issues The human-readable localization messages of corresponding `issueCodes`
         * @property {module:svgcanvas.IssueCode[]} issueCodes
         * @property {string} exportWindowName
         */

        /**
         * Generates a PDF based on the current image, then calls "exportedPDF" with
         * an object including the string, the data URL, and any issues found.
         * @function module:svgcanvas.SvgCanvas#exportPDF
         * @param {string} [exportWindowName] Will also be used for the download file name here
         * @param {external:jsPDF.OutputType} [outputType="dataurlstring"]
         * @fires module:svgcanvas.SvgCanvas#event:exportedPDF
         * @returns {Promise<module:svgcanvas.PDFExportedResults>} Resolves to {@link module:svgcanvas.PDFExportedResults}
         */
        this.exportPDF = async function(
            exportWindowName,
            outputType = isChrome() ? 'save' : undefined
        ) {
            if (!window.jsPDF) {
                // Todo: Switch to `import()` when widely supported and available (also allow customization of path)
                await importScript([
                    // We do not currently have these paths configurable as they are
                    //   currently global-only, so not Rolled-up
                    'jspdf/underscore-min.js',
                    'jspdf/jspdf.min.js'
                ])

                const modularVersion =
                    !('svgEditor' in window) ||
                    !window.svgEditor ||
                    window.svgEditor.modules !== false
                // Todo: Switch to `import()` when widely supported and available (also allow customization of path)
                await importScript(
                    curConfig.jspdfPath + 'jspdf.plugin.svgToPdf.js',
                    {
                        type: modularVersion ? 'module' : 'text/javascript'
                    }
                )
                // await importModule('jspdf/jspdf.plugin.svgToPdf.js');
            }

            const res = getResolution()
            const orientation = res.w > res.h ? 'landscape' : 'portrait'
            const unit = 'pt' // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for export purposes

            // Todo: Give options to use predefined jsPDF formats like "a4", etc. from pull-down (with option to keep customizable)
            const doc = jsPDF({
                orientation,
                unit,
                format: [res.w, res.h]
                // , compressPdf: true
            })
            const docTitle = getDocumentTitle()
            doc.setProperties({
                title: docTitle /* ,
    subject: '',
    author: '',
    keywords: '',
    creator: '' */
            })
            const { issues, issueCodes } = getIssues()
            const svg = this.svgCanvasToString()
            doc.addSVG(svg, 0, 0)

            // doc.output('save'); // Works to open in a new
            //  window; todo: configure this and other export
            //  options to optionally work in this manner as
            //  opposed to opening a new tab
            outputType = outputType || 'dataurlstring'
            const obj = {
                svg,
                issues,
                issueCodes,
                exportWindowName,
                outputType
            }
            obj.output = doc.output(
                outputType,
                outputType === 'save'
                    ? exportWindowName || 'svg.pdf'
                    : undefined
            )
            call('exportedPDF', obj)
            return obj
        }

        /**
         * Returns the current drawing as raw SVG XML text.
         * @function module:svgcanvas.SvgCanvas#getSvgString
         * @returns {string} The current drawing as raw SVG XML text.
         */
        this.getSvgString = function() {
            saveOptions.apply = false
            return this.svgCanvasToString()
        }

        /**
         * This function determines whether to use a nonce in the prefix, when
         * generating IDs for future documents in SVG-Edit.
         * If you're controlling SVG-Edit externally, and want randomized IDs, call
         * this BEFORE calling `svgCanvas.setSvgString`.
         * @function module:svgcanvas.SvgCanvas#randomizeIds
         * @param {boolean} [enableRandomization] If true, adds a nonce to the prefix. Thus
         * `svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true)`
         * @returns {void}
         */
        this.randomizeIds = function(enableRandomization) {
            if (arguments.length > 0 && enableRandomization === false) {
                draw.randomizeIds(false, getCurrentDrawing())
            } else {
                draw.randomizeIds(true, getCurrentDrawing())
            }
        }

        /**
         * Ensure each element has a unique ID.
         * @function module:svgcanvas.SvgCanvas#uniquifyElems
         * @param {Element} g - The parent element of the tree to give unique IDs
         * @returns {void}
         */
        const uniquifyElems = (this.uniquifyElems = function(g) {
            const ids = {}
            // TODO: Handle markers and connectors. These are not yet re-identified properly
            // as their referring elements do not get remapped.
            //
            // <marker id='se_marker_end_svg_7'/>
            // <polyline id='svg_7' se:connector='svg_1 svg_6' marker-end='url(#se_marker_end_svg_7)'/>
            //
            // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute
            // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute
            const refElems = [
                'filter',
                'linearGradient',
                'pattern',
                'radialGradient',
                'symbol',
                'textPath',
                'use'
            ]

            walkTree(g, function(n) {
                // if it's an element node
                if (n.nodeType === 1) {
                    // and the element has an ID
                    if (n.id) {
                        // and we haven't tracked this ID yet
                        if (!(n.id in ids)) {
                            // add this id to our map
                            ids[n.id] = { elem: null, attrs: [], hrefs: [] }
                        }
                        ids[n.id].elem = n
                    }

                    // now search for all attributes on this element that might refer
                    // to other elements
                    $.each(refAttrs, function(i, attr) {
                        const attrnode = n.getAttributeNode(attr)
                        if (attrnode) {
                            // the incoming file has been sanitized, so we should be able to safely just strip off the leading #
                            const url = getUrlFromAttr(attrnode.value),
                                refid = url ? url.substr(1) : null
                            if (refid) {
                                if (!(refid in ids)) {
                                    // add this id to our map
                                    ids[refid] = {
                                        elem: null,
                                        attrs: [],
                                        hrefs: []
                                    }
                                }
                                ids[refid].attrs.push(attrnode)
                            }
                        }
                    })

                    // check xlink:href now
                    const href = getHref(n)
                    // TODO: what if an <image> or <a> element refers to an element internally?
                    if (href && refElems.includes(n.nodeName)) {
                        const refid = href.substr(1)
                        if (refid) {
                            if (!(refid in ids)) {
                                // add this id to our map
                                ids[refid] = {
                                    elem: null,
                                    attrs: [],
                                    hrefs: []
                                }
                            }
                            ids[refid].hrefs.push(n)
                        }
                    }
                }
            })

            // in ids, we now have a map of ids, elements and attributes, let's re-identify
            for (const oldid in ids) {
                if (!oldid) {
                    continue
                }
                const { elem } = ids[oldid]
                if (elem) {
                    const newid = getNextId()

                    // assign element its new id
                    elem.id = newid

                    // remap all url() attributes
                    const { attrs } = ids[oldid]
                    let j = attrs.length
                    while (j--) {
                        const attr = attrs[j]
                        attr.ownerElement.setAttribute(
                            attr.name,
                            'url(#' + newid + ')'
                        )
                    }

                    // remap all href attributes
                    const hreffers = ids[oldid].hrefs
                    let k = hreffers.length
                    while (k--) {
                        const hreffer = hreffers[k]
                        setHref(hreffer, '#' + newid)
                    }
                }
            }
        })

        /**
         * Assigns reference data for each use element.
         * @function module:svgcanvas.SvgCanvas#setUseData
         * @param {Element} parent
         * @returns {void}
         */
        const setUseData = (this.setUseData = function(parent) {
            let elems = $(parent)

            if (parent.tagName !== 'use') {
                elems = elems.find('use')
            }

            elems.each(function() {
                const id = getHref(this).substr(1)
                const refElem = getElem(id)
                if (!refElem) {
                    return
                }
                $(this).data('ref', refElem)
                if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {
                    $(this)
                        .data('symbol', refElem)
                        .data('ref', refElem)
                }
            })
        })

        /**
         * Converts gradients from userSpaceOnUse to objectBoundingBox.
         * @function module:svgcanvas.SvgCanvas#convertGradients
         * @param {Element} elem
         * @returns {void}
         */
        const convertGradients = (this.convertGradients = function(elem) {
            let elems = $(elem).find('linearGradient, radialGradient')
            if (!elems.length && isWebkit()) {
                // Bug in webkit prevents regular *Gradient selector search
                elems = $(elem)
                    .find('*')
                    .filter(function() {
                        return this.tagName.includes('Gradient')
                    })
            }

            elems.each(function() {
                const grad = this // eslint-disable-line consistent-this
                if ($(grad).attr('gradientUnits') === 'userSpaceOnUse') {
                    // TODO: Support more than one element with this ref by duplicating parent grad
                    const fillStrokeElems = $(svgcontent).find(
                        '[fill="url(#' +
                            grad.id +
                            ')"],[stroke="url(#' +
                            grad.id +
                            ')"]'
                    )
                    if (!fillStrokeElems.length) {
                        return
                    }

                    // get object's bounding box
                    const bb = utilsGetBBox(fillStrokeElems[0])

                    // This will occur if the element is inside a <defs> or a <symbol>,
                    // in which we shouldn't need to convert anyway.
                    if (!bb) {
                        return
                    }

                    if (grad.tagName === 'linearGradient') {
                        const gCoords = $(grad).attr(['x1', 'y1', 'x2', 'y2'])

                        // If has transform, convert
                        const tlist = grad.gradientTransform.baseVal
                        if (tlist && tlist.numberOfItems > 0) {
                            const m = transformListToTransform(tlist).matrix
                            const pt1 = transformPoint(
                                gCoords.x1,
                                gCoords.y1,
                                m
                            )
                            const pt2 = transformPoint(
                                gCoords.x2,
                                gCoords.y2,
                                m
                            )

                            gCoords.x1 = pt1.x
                            gCoords.y1 = pt1.y
                            gCoords.x2 = pt2.x
                            gCoords.y2 = pt2.y
                            grad.removeAttribute('gradientTransform')
                        }

                        $(grad).attr({
                            x1: (gCoords.x1 - bb.x) / bb.width,
                            y1: (gCoords.y1 - bb.y) / bb.height,
                            x2: (gCoords.x2 - bb.x) / bb.width,
                            y2: (gCoords.y2 - bb.y) / bb.height
                        })
                        grad.removeAttribute('gradientUnits')
                    }
                    // else {
                    //   Note: radialGradient elements cannot be easily converted
                    //   because userSpaceOnUse will keep circular gradients, while
                    //   objectBoundingBox will x/y scale the gradient according to
                    //   its bbox.
                    //
                    //   For now we'll do nothing, though we should probably have
                    //   the gradient be updated as the element is moved, as
                    //   inkscape/illustrator do.
                    //
                    //   const gCoords = $(grad).attr(['cx', 'cy', 'r']);
                    //
                    //   $(grad).attr({
                    //     cx: (gCoords.cx - bb.x) / bb.width,
                    //     cy: (gCoords.cy - bb.y) / bb.height,
                    //     r: gCoords.r
                    //   });
                    //
                    //   grad.removeAttribute('gradientUnits');
                    // }
                }
            })
        })

        /**
         * Converts selected/given `<use>` or child SVG element to a group.
         * @function module:svgcanvas.SvgCanvas#convertToGroup
         * @param {Element} elem
         * @fires module:svgcanvas.SvgCanvas#event:selected
         * @returns {void}
         */
        const convertToGroup = (this.convertToGroup = function(elem) {
            if (!elem) {
                elem = selectedElements[0]
            }
            const $elem = $(elem)
            const batchCmd = new BatchCommand()
            let ts

            if ($elem.data('gsvg')) {
                // Use the gsvg as the new group
                const svg = elem.firstChild
                const pt = $(svg).attr(['x', 'y'])

                $(elem.firstChild.firstChild).unwrap()
                $(elem).removeData('gsvg')

                const tlist = getTransformList(elem)
                const xform = svgroot.createSVGTransform()
                xform.setTranslate(pt.x, pt.y)
                tlist.appendItem(xform)
                recalculateDimensions(elem)
                call('selected', [elem])
            } else if ($elem.data('symbol')) {
                elem = $elem.data('symbol')

                ts = $elem.attr('transform')
                const pos = $elem.attr(['x', 'y'])

                const vb = elem.getAttribute('viewBox')

                if (vb) {
                    const nums = vb.split(' ')
                    pos.x -= Number(nums[0])
                    pos.y -= Number(nums[1])
                }

                // Not ideal, but works
                ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'

                const prev = $elem.prev()

                // Remove <use> element
                batchCmd.addSubCommand(
                    new RemoveElementCommand(
                        $elem[0],
                        $elem[0].nextSibling,
                        $elem[0].parentNode
                    )
                )
                $elem.remove()

                // See if other elements reference this symbol
                const hasMore = $(svgcontent).find('use:data(symbol)').length

                const g = svgdoc.createElementNS(NS.SVG, 'g')
                const childs = elem.childNodes

                let i
                for (i = 0; i < childs.length; i++) {
                    g.append(childs[i].cloneNode(true))
                }

                // Duplicate the gradients for Gecko, since they weren't included in the <symbol>
                if (isGecko()) {
                    const dupeGrads = $(findDefs())
                        .children('linearGradient,radialGradient,pattern')
                        .clone()
                    $(g).append(dupeGrads)
                }

                if (ts) {
                    g.setAttribute('transform', ts)
                }

                const parent = elem.parentNode

                uniquifyElems(g)

                // Put the dupe gradients back into <defs> (after uniquifying them)
                if (isGecko()) {
                    $(findDefs()).append(
                        $(g).find('linearGradient,radialGradient,pattern')
                    )
                }

                // now give the g itself a new id
                g.id = getNextId()

                prev.after(g)

                if (parent) {
                    if (!hasMore) {
                        // remove symbol/svg element
                        const { nextSibling } = elem
                        elem.remove()
                        batchCmd.addSubCommand(
                            new RemoveElementCommand(elem, nextSibling, parent)
                        )
                    }
                    batchCmd.addSubCommand(new InsertElementCommand(g))
                }

                setUseData(g)

                if (isGecko()) {
                    convertGradients(findDefs())
                } else {
                    convertGradients(g)
                }

                // recalculate dimensions on the top-level children so that unnecessary transforms
                // are removed
                walkTreePost(g, function(n) {
                    try {
                        recalculateDimensions(n)
                    } catch (e) {
                        console.log(e) // eslint-disable-line no-console
                    }
                })

                // Give ID for any visible element missing one
                $(g)
                    .find(visElems)
                    .each(function() {
                        if (!this.id) {
                            this.id = getNextId()
                        }
                    })

                selectOnly([g])

                const cm = pushGroupProperties(g, true)
                if (cm) {
                    batchCmd.addSubCommand(cm)
                }

                addCommandToHistory(batchCmd)
            } else {
                console.log('Unexpected element to ungroup:', elem) // eslint-disable-line no-console
            }
        })

        /**
         * This function sets the current drawing as the input SVG XML.
         * @function module:svgcanvas.SvgCanvas#setSvgString
         * @param {string} xmlString - The SVG as XML text.
         * @param {boolean} [preventUndo=false] - Indicates if we want to do the
         * changes without adding them to the undo stack - e.g. for initializing a
         * drawing on page load.
         * @fires module:svgcanvas.SvgCanvas#event:setnonce
         * @fires module:svgcanvas.SvgCanvas#event:unsetnonce
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {boolean} This function returns `false` if the set was
         *     unsuccessful, `true` otherwise.
         */
        this.setSvgString = function(xmlString, preventUndo) {
            try {
                // convert string into XML document
                const newDoc = text2xml(xmlString)
                if (
                    newDoc.firstElementChild &&
                    newDoc.firstElementChild.namespaceURI !== NS.SVG
                ) {
                    return false
                }

                this.prepareSvg(newDoc)

                const batchCmd = new BatchCommand('Change Source')

                // remove old svg document
                const { nextSibling } = svgcontent
                const oldzoom = svgroot.removeChild(svgcontent)
                batchCmd.addSubCommand(
                    new RemoveElementCommand(oldzoom, nextSibling, svgroot)
                )

                // set new svg document
                // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
                if (svgdoc.adoptNode) {
                    svgcontent = svgdoc.adoptNode(newDoc.documentElement)
                } else {
                    svgcontent = svgdoc.importNode(newDoc.documentElement, true)
                }

                svgroot.append(svgcontent)
                const content = $(svgcontent)

                canvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix)

                // retrieve or set the nonce
                const nonce = getCurrentDrawing().getNonce()
                if (nonce) {
                    call('setnonce', nonce)
                } else {
                    call('unsetnonce')
                }

                // change image href vals if possible
                content.find('image').each(function() {
                    const image = this // eslint-disable-line consistent-this
                    preventClickDefault(image)
                    const val = getHref(this)
                    if (val) {
                        if (val.startsWith('data:')) {
                            // Check if an SVG-edit data URI
                            const m = val.match(/svgedit_url=(?<url>.*?);/)
                            if (m) {
                                const url = decodeURIComponent(m.groups.url)
                                $(new Image())
                                    .load(function() {
                                        image.setAttributeNS(
                                            NS.XLINK,
                                            'xlink:href',
                                            url
                                        )
                                    })
                                    .attr('src', url)
                            }
                        }
                        // Add to encodableImages if it loads
                        canvas.embedImage(val)
                    }
                })

                // Wrap child SVGs in group elements
                content.find('svg').each(function() {
                    // Skip if it's in a <defs>
                    if ($(this).closest('defs').length) {
                        return
                    }

                    uniquifyElems(this)

                    // Check if it already has a gsvg group
                    const pa = this.parentNode
                    if (pa.childNodes.length === 1 && pa.nodeName === 'g') {
                        $(pa).data('gsvg', this)
                        pa.id = pa.id || getNextId()
                    } else {
                        groupSvgElem(this)
                    }
                })

                // For Firefox: Put all paint elems in defs
                if (isGecko()) {
                    content
                        .find('linearGradient, radialGradient, pattern')
                        .appendTo(findDefs())
                }

                // Set ref element for <use> elements

                // TODO: This should also be done if the object is re-added through "redo"
                setUseData(content)

                convertGradients(content[0])

                const attrs = {
                    id: 'svgcontent',
                    overflow: curConfig.show_outside_canvas
                        ? 'visible'
                        : 'hidden'
                }

                let percs = false

                // determine proper size
                if (content.attr('viewBox')) {
                    const vb = content.attr('viewBox').split(' ')
                    attrs.width = vb[2]
                    attrs.height = vb[3]
                    // handle content that doesn't have a viewBox
                } else {
                    $.each(['width', 'height'], function(i, dim) {
                        // Set to 100 if not given
                        const val = content.attr(dim) || '100%'

                        if (String(val).substr(-1) === '%') {
                            // Use user units if percentage given
                            percs = true
                        } else {
                            attrs[dim] = convertToNum(dim, val)
                        }
                    })
                }

                // identify layers
                draw.identifyLayers()

                // Give ID for any visible layer children missing one
                content
                    .children()
                    .find(visElems)
                    .each(function() {
                        if (!this.id) {
                            this.id = getNextId()
                        }
                    })

                // Percentage width/height, so let's base it on visible elements
                if (percs) {
                    const bb = getStrokedBBoxDefaultVisible()
                    attrs.width = bb.width + bb.x
                    attrs.height = bb.height + bb.y
                }

                // Just in case negative numbers are given or
                // result from the percs calculation
                if (attrs.width <= 0) {
                    attrs.width = 100
                }
                if (attrs.height <= 0) {
                    attrs.height = 100
                }

                content.attr(attrs)
                this.contentW = attrs.width
                this.contentH = attrs.height

                batchCmd.addSubCommand(new InsertElementCommand(svgcontent))
                // update root to the correct size
                const changes = content.attr(['width', 'height'])
                batchCmd.addSubCommand(
                    new ChangeElementCommand(svgroot, changes)
                )

                // reset zoom
                currentZoom = 1

                // reset transform lists
                resetListMap()
                clearSelection()
                pathModule.clearData()
                svgroot.append(selectorManager.selectorParentGroup)

                if (!preventUndo) addCommandToHistory(batchCmd)
                call('changed', [svgcontent])
            } catch (e) {
                console.log(e) // eslint-disable-line no-console
                return false
            }

            return true
        }

        /**
         * This function imports the input SVG XML as a `<symbol>` in the `<defs>`, then adds a
         * `<use>` to the current layer.
         * @function module:svgcanvas.SvgCanvas#importSvgString
         * @param {string} xmlString - The SVG as XML text.
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {null|Element} This function returns null if the import was unsuccessful, or the element otherwise.
         * @todo
         * - properly handle if namespace is introduced by imported content (must add to svgcontent
         * and update all prefixes in the imported node)
         * - properly handle recalculating dimensions, `recalculateDimensions()` doesn't handle
         * arbitrary transform lists, but makes some assumptions about how the transform list
         * was obtained
         */
        this.importSvgString = function(xmlString) {
            let j, ts, useEl
            try {
                // Get unique ID
                const uid = encode64(xmlString.length + xmlString).substr(0, 32)

                let useExisting = false
                // Look for symbol and make sure symbol exists in image
                if (importIds[uid]) {
                    if ($(importIds[uid].symbol).parents('#svgroot').length) {
                        useExisting = true
                    }
                }

                const batchCmd = new BatchCommand('Import Image')
                let symbol
                if (useExisting) {
                    ;({ symbol } = importIds[uid])
                    ts = importIds[uid].xform
                } else {
                    // convert string into XML document
                    const newDoc = text2xml(xmlString)

                    this.prepareSvg(newDoc)

                    // import new svg document into our document
                    let svg
                    // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
                    if (svgdoc.adoptNode) {
                        svg = svgdoc.adoptNode(newDoc.documentElement)
                    } else {
                        svg = svgdoc.importNode(newDoc.documentElement, true)
                    }

                    uniquifyElems(svg)

                    const innerw = convertToNum(
                            'width',
                            svg.getAttribute('width')
                        ),
                        innerh = convertToNum(
                            'height',
                            svg.getAttribute('height')
                        ),
                        innervb = svg.getAttribute('viewBox'),
                        // if no explicit viewbox, create one out of the width and height
                        vb = innervb
                            ? innervb.split(' ')
                            : [0, 0, innerw, innerh]
                    for (j = 0; j < 4; ++j) {
                        vb[j] = Number(vb[j])
                    }

                    // TODO: properly handle preserveAspectRatio
                    const // canvasw = +svgcontent.getAttribute('width'),
                        canvash = Number(svgcontent.getAttribute('height'))
                    // imported content should be 1/3 of the canvas on its largest dimension

                    if (innerh > innerw) {
                        ts = 'scale(' + canvash / 3 / vb[3] + ')'
                    } else {
                        ts = 'scale(' + canvash / 3 / vb[2] + ')'
                    }

                    // Hack to make recalculateDimensions understand how to scale
                    ts = 'translate(0) ' + ts + ' translate(0)'

                    symbol = svgdoc.createElementNS(NS.SVG, 'symbol')
                    const defs = findDefs()

                    if (isGecko()) {
                        // Move all gradients into root for Firefox, workaround for this bug:
                        // https://bugzilla.mozilla.org/show_bug.cgi?id=353575
                        // TODO: Make this properly undo-able.
                        $(svg)
                            .find('linearGradient, radialGradient, pattern')
                            .appendTo(defs)
                    }

                    while (svg.firstChild) {
                        const first = svg.firstChild
                        symbol.append(first)
                    }
                    const attrs = svg.attributes
                    for (const attr of attrs) {
                        // Ok for `NamedNodeMap`
                        symbol.setAttribute(attr.nodeName, attr.value)
                    }
                    symbol.id = getNextId()

                    // Store data
                    importIds[uid] = {
                        symbol,
                        xform: ts
                    }

                    findDefs().append(symbol)
                    batchCmd.addSubCommand(new InsertElementCommand(symbol))
                }

                useEl = svgdoc.createElementNS(NS.SVG, 'use')
                useEl.id = getNextId()
                setHref(useEl, '#' + symbol.id)
                ;(currentGroup || getCurrentDrawing().getCurrentLayer()).append(
                    useEl
                )
                batchCmd.addSubCommand(new InsertElementCommand(useEl))
                clearSelection()

                useEl.setAttribute('transform', ts)
                recalculateDimensions(useEl)
                $(useEl)
                    .data('symbol', symbol)
                    .data('ref', symbol)
                addToSelection([useEl])

                // TODO: Find way to add this in a recalculateDimensions-parsable way
                // if (vb[0] !== 0 || vb[1] !== 0) {
                //   ts = 'translate(' + (-vb[0]) + ',' + (-vb[1]) + ') ' + ts;
                // }
                addCommandToHistory(batchCmd)
                call('changed', [svgcontent])
            } catch (e) {
                console.log(e) // eslint-disable-line no-console
                return null
            }

            // we want to return the element so we can automatically select it
            return useEl
        }

        // Could deprecate, but besides external uses, their usage makes clear that
        //  canvas is a dependency for all of these
        const dr = {
            identifyLayers,
            createLayer,
            cloneLayer,
            deleteCurrentLayer,
            setCurrentLayer,
            renameCurrentLayer,
            setCurrentLayerPosition,
            setLayerVisibility,
            moveSelectedToLayer,
            mergeLayer,
            mergeAllLayers,
            leaveContext,
            setContext
        }
        Object.entries(dr).forEach(([prop, propVal]) => {
            canvas[prop] = propVal
        })
        draw.init(
            /**
             * @implements {module:draw.DrawCanvasInit}
             */
            {
                pathActions,
                getCurrentGroup() {
                    return currentGroup
                },
                setCurrentGroup(cg) {
                    currentGroup = cg
                },
                getSelectedElements,
                getSVGContent,
                undoMgr,
                elData,
                getCurrentDrawing,
                clearSelection,
                call,
                addCommandToHistory,
                /**
                 * @fires module:svgcanvas.SvgCanvas#event:changed
                 * @returns {void}
                 */
                changeSVGContent() {
                    call('changed', [svgcontent])
                }
            }
        )

        /**
         * Group: Document functions
         */

        /**
         * Clears the current document. This is not an undoable action.
         * @function module:svgcanvas.SvgCanvas#clear
         * @fires module:svgcanvas.SvgCanvas#event:cleared
         * @returns {void}
         */
        this.clear = function() {
            pathActions.clear()

            clearSelection()

            // clear the svgcontent node
            canvas.clearSvgContentElement()

            // create new document
            canvas.current_drawing_ = new draw.Drawing(svgcontent)

            // create empty first layer
            canvas.createLayer('')

            // clear the undo stack
            canvas.undoMgr.resetUndoStack()

            // reset the selector manager
            selectorManager.initGroup()

            // reset the rubber band box
            rubberBox = selectorManager.getRubberBandBox()

            call('cleared')
        }

        // Alias function
        this.linkControlPoints = pathActions.linkControlPoints

        /**
         * @function module:svgcanvas.SvgCanvas#getContentElem
         * @returns {Element} The content DOM element
         */
        this.getContentElem = function() {
            return svgcontent
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getRootElem
         * @returns {SVGSVGElement} The root DOM element
         */
        this.getRootElem = function() {
            return svgroot
        }

        /**
         * @typedef {PlainObject} DimensionsAndZoom
         * @property {Float} w Width
         * @property {Float} h Height
         * @property {Float} zoom Zoom
         */

        /**
         * @function module:svgcanvas.SvgCanvas#getResolution
         * @returns {DimensionsAndZoom} The current dimensions and zoom level in an object
         */
        const getResolution = (this.getResolution = function() {
            //    const vb = svgcontent.getAttribute('viewBox').split(' ');
            //    return {w:vb[2], h:vb[3], zoom: currentZoom};

            const w = svgcontent.getAttribute('width') / currentZoom
            const h = svgcontent.getAttribute('height') / currentZoom

            return {
                w,
                h,
                zoom: currentZoom
            }
        })

        /**
         * @function module:svgcanvas.SvgCanvas#getSnapToGrid
         * @returns {boolean} The current snap to grid setting
         */
        this.getSnapToGrid = function() {
            return curConfig.gridSnapping
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getVersion
         * @returns {string} A string which describes the revision number of SvgCanvas.
         */
        this.getVersion = function() {
            return 'svgcanvas.js ($Rev$)'
        }

        /**
         * Update interface strings with given values.
         * @function module:svgcanvas.SvgCanvas#setUiStrings
         * @param {module:path.uiStrings} strs - Object with strings (see the [locales API]{@link module:locale.LocaleStrings} and the [tutorial]{@tutorial LocaleDocs})
         * @returns {void}
         */
        this.setUiStrings = function(strs) {
            Object.assign(uiStrings, strs.notification)
            $ = jQueryPluginDBox($, strs.common)
            pathModule.setUiStrings(strs)
        }

        /**
         * Update configuration options with given values.
         * @function module:svgcanvas.SvgCanvas#setConfig
         * @param {module:SVGEditor.Config} opts - Object with options
         * @returns {void}
         */
        this.setConfig = function(opts) {
            Object.assign(curConfig, opts)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getTitle
         * @param {Element} [elem]
         * @returns {string|void} the current group/SVG's title contents or
         * `undefined` if no element is passed nd there are no selected elements.
         */
        this.getTitle = function(elem) {
            elem = elem || selectedElements[0]
            if (!elem) {
                return undefined
            }
            elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem
            const childs = elem.childNodes
            for (const child of childs) {
                if (child.nodeName === 'title') {
                    return child.textContent
                }
            }
            return ''
        }

        /**
         * Sets the group/SVG's title content.
         * @function module:svgcanvas.SvgCanvas#setGroupTitle
         * @param {string} val
         * @todo Combine this with `setDocumentTitle`
         * @returns {void}
         */
        this.setGroupTitle = function(val) {
            let elem = selectedElements[0]
            elem = $(elem).data('gsvg') || elem

            const ts = $(elem).children('title')

            const batchCmd = new BatchCommand('Set Label')

            let title
            if (!val.length) {
                // Remove title element
                const tsNextSibling = ts.nextSibling
                batchCmd.addSubCommand(
                    new RemoveElementCommand(ts[0], tsNextSibling, elem)
                )
                ts.remove()
            } else if (ts.length) {
                // Change title contents
                title = ts[0]
                batchCmd.addSubCommand(
                    new ChangeElementCommand(title, {
                        '#text': title.textContent
                    })
                )
                title.textContent = val
            } else {
                // Add title element
                title = svgdoc.createElementNS(NS.SVG, 'title')
                title.textContent = val
                $(elem).prepend(title)
                batchCmd.addSubCommand(new InsertElementCommand(title))
            }

            addCommandToHistory(batchCmd)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getDocumentTitle
         * @returns {string|void} The current document title or an empty string if not found
         */
        const getDocumentTitle = (this.getDocumentTitle = function() {
            return canvas.getTitle(svgcontent)
        })

        /**
         * Adds/updates a title element for the document with the given name.
         * This is an undoable action.
         * @function module:svgcanvas.SvgCanvas#setDocumentTitle
         * @param {string} newTitle - String with the new title
         * @returns {void}
         */
        this.setDocumentTitle = function(newTitle) {
            const childs = svgcontent.childNodes
            let docTitle = false,
                oldTitle = ''

            const batchCmd = new BatchCommand('Change Image Title')

            for (const child of childs) {
                if (child.nodeName === 'title') {
                    docTitle = child
                    oldTitle = docTitle.textContent
                    break
                }
            }
            if (!docTitle) {
                docTitle = svgdoc.createElementNS(NS.SVG, 'title')
                svgcontent.insertBefore(docTitle, svgcontent.firstChild)
                // svgcontent.firstChild.before(docTitle); // Ok to replace above with this?
            }

            if (newTitle.length) {
                docTitle.textContent = newTitle
            } else {
                // No title given, so element is not necessary
                docTitle.remove()
            }
            batchCmd.addSubCommand(
                new ChangeElementCommand(docTitle, { '#text': oldTitle })
            )
            addCommandToHistory(batchCmd)
        }

        /**
         * Returns the editor's namespace URL, optionally adding it to the root element.
         * @function module:svgcanvas.SvgCanvas#getEditorNS
         * @param {boolean} [add] - Indicates whether or not to add the namespace value
         * @returns {string} The editor's namespace URL
         */
        this.getEditorNS = function(add) {
            if (add) {
                svgcontent.setAttribute('xmlns:se', NS.SE)
            }
            return NS.SE
        }

        /**
         * Changes the document's dimensions to the given size.
         * @function module:svgcanvas.SvgCanvas#setResolution
         * @param {Float|"fit"} x - Number with the width of the new dimensions in user units.
         * Can also be the string "fit" to indicate "fit to content".
         * @param {Float} y - Number with the height of the new dimensions in user units.
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {boolean} Indicates if resolution change was successful.
         * It will fail on "fit to content" option with no content to fit to.
         */
        this.setResolution = function(x, y) {
            const res = getResolution()
            const { w, h } = res
            let batchCmd

            if (x === 'fit') {
                // Get bounding box
                const bbox = getStrokedBBoxDefaultVisible()

                if (bbox) {
                    batchCmd = new BatchCommand('Fit Canvas to Content')
                    const visEls = getVisibleElements()
                    addToSelection(visEls)
                    const dx = [],
                        dy = []
                    $.each(visEls, function(i, item) {
                        dx.push(bbox.x * -1)
                        dy.push(bbox.y * -1)
                    })

                    const cmd = canvas.moveSelectedElements(dx, dy, true)
                    batchCmd.addSubCommand(cmd)
                    clearSelection()

                    x = Math.round(bbox.width)
                    y = Math.round(bbox.height)
                } else {
                    return false
                }
            }
            if (x !== w || y !== h) {
                if (!batchCmd) {
                    batchCmd = new BatchCommand('Change Image Dimensions')
                }

                x = convertToNum('width', x)
                y = convertToNum('height', y)

                svgcontent.setAttribute('width', x)
                svgcontent.setAttribute('height', y)

                this.contentW = x
                this.contentH = y
                batchCmd.addSubCommand(
                    new ChangeElementCommand(svgcontent, {
                        width: w,
                        height: h
                    })
                )

                svgcontent.setAttribute(
                    'viewBox',
                    [0, 0, x / currentZoom, y / currentZoom].join(' ')
                )
                batchCmd.addSubCommand(
                    new ChangeElementCommand(svgcontent, {
                        viewBox: ['0 0', w, h].join(' ')
                    })
                )

                addCommandToHistory(batchCmd)
                call('changed', [svgcontent])
            }
            return true
        }

        /**
         * @typedef {module:jQueryAttr.Attributes} module:svgcanvas.ElementPositionInCanvas
         * @property {Float} x
         * @property {Float} y
         */

        /**
         * @function module:svgcanvas.SvgCanvas#getOffset
         * @returns {module:svgcanvas.ElementPositionInCanvas} An object with `x`, `y` values indicating the svgcontent element's
         * position in the editor's canvas.
         */
        this.getOffset = function() {
            return $(svgcontent).attr(['x', 'y'])
        }

        /**
         * @typedef {PlainObject} module:svgcanvas.ZoomAndBBox
         * @property {Float} zoom
         * @property {module:utilities.BBoxObject} bbox
         */
        /**
         * Sets the zoom level on the canvas-side based on the given value.
         * @function module:svgcanvas.SvgCanvas#setBBoxZoom
         * @param {"selection"|"canvas"|"content"|"layer"|module:SVGEditor.BBoxObjectWithFactor} val - Bounding box object to zoom to or string indicating zoom option. Note: the object value type is defined in `svg-editor.js`
         * @param {Integer} editorW - The editor's workarea box's width
         * @param {Integer} editorH - The editor's workarea box's height
         * @returns {module:svgcanvas.ZoomAndBBox|void}
         */
        this.setBBoxZoom = function(val, editorW, editorH) {
            let spacer = 0.85
            let bb
            const calcZoom = function(bb) {
                // eslint-disable-line no-shadow
                if (!bb) {
                    return false
                }
                const wZoom =
                    Math.round((editorW / bb.width) * 100 * spacer) / 100
                const hZoom =
                    Math.round((editorH / bb.height) * 100 * spacer) / 100
                const zoom = Math.min(wZoom, hZoom)
                canvas.setZoom(zoom)
                return { zoom, bbox: bb }
            }

            if (typeof val === 'object') {
                bb = val
                if (bb.width === 0 || bb.height === 0) {
                    const newzoom = bb.zoom ? bb.zoom : currentZoom * bb.factor
                    canvas.setZoom(newzoom)
                    return { zoom: currentZoom, bbox: bb }
                }
                return calcZoom(bb)
            }

            switch (val) {
                case 'selection': {
                    if (!selectedElements[0]) {
                        return undefined
                    }
                    const selectedElems = $.map(selectedElements, function(n) {
                        if (n) {
                            return n
                        }
                        return undefined
                    })
                    bb = getStrokedBBoxDefaultVisible(selectedElems)
                    break
                }
                case 'canvas': {
                    const res = getResolution()
                    spacer = 0.95
                    bb = { width: res.w, height: res.h, x: 0, y: 0 }
                    break
                }
                case 'content':
                    bb = getStrokedBBoxDefaultVisible()
                    break
                case 'layer':
                    bb = getStrokedBBoxDefaultVisible(
                        getVisibleElements(
                            getCurrentDrawing().getCurrentLayer()
                        )
                    )
                    break
                default:
                    return undefined
            }
            return calcZoom(bb)
        }

        /**
         * The zoom level has changed. Supplies the new zoom level as a number (not percentage).
         * @event module:svgcanvas.SvgCanvas#event:ext_zoomChanged
         * @type {Float}
         */
        /**
         * The bottom panel was updated
         * @event module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate
         * @type {PlainObject}
         * @property {boolean} nofill Indicates fill is disabled
         * @property {boolean} nostroke Indicates stroke is disabled
         */
        /**
         * The element selection has changed (elements were added/removed from selection)
         * @event module:svgcanvas.SvgCanvas#event:ext_selectedChanged
         * @type {PlainObject}
         * @property {Element[]} elems Array of the newly selected elements
         * @property {Element|null} selectedElement The single selected element
         * @property {boolean} multiselected Indicates whether one or more elements were selected
         */
        /**
         * Called when part of element is in process of changing, generally on
         * mousemove actions like rotate, move, etc.
         * @event module:svgcanvas.SvgCanvas#event:ext_elementTransition
         * @type {PlainObject}
         * @property {Element[]} elems Array of transitioning elements
         */
        /**
         * One or more elements were changed
         * @event module:svgcanvas.SvgCanvas#event:ext_elementChanged
         * @type {PlainObject}
         * @property {Element[]} elems Array of the affected elements
         */
        /**
         * Invoked as soon as the locale is ready
         * @event module:svgcanvas.SvgCanvas#event:ext_langReady
         * @type {PlainObject}
         * @property {string} lang The two-letter language code
         * @property {module:SVGEditor.uiStrings} uiStrings
         * @property {module:SVGEditor~ImportLocale} importLocale
         */
        /**
         * The language was changed. Two-letter code of the new language.
         * @event module:svgcanvas.SvgCanvas#event:ext_langChanged
         * @type {string}
         */
        /**
         * Means for an extension to add locale data. The two-letter language code.
         * @event module:svgcanvas.SvgCanvas#event:ext_addLangData
         * @type {PlainObject}
         * @property {string} lang
         * @property {module:SVGEditor~ImportLocale} importLocale
         */
        /**
         * Called when new image is created
         * @event module:svgcanvas.SvgCanvas#event:ext_onNewDocument
         * @type {void}
         */
        /**
         * Called when sidepanel is resized or toggled
         * @event module:svgcanvas.SvgCanvas#event:ext_workareaResized
         * @type {void}
         */
        /**
         * Called upon addition of the extension, or, if svgicons are set,
         * after the icons are ready when extension SVG icons have loaded.
         * @event module:svgcanvas.SvgCanvas#event:ext_callback
         * @type {void}
         */

        /**
         * Sets the zoom to the given level.
         * @function module:svgcanvas.SvgCanvas#setZoom
         * @param {Float} zoomLevel - Float indicating the zoom level to change to
         * @fires module:svgcanvas.SvgCanvas#event:ext_zoomChanged
         * @returns {void}
         */
        this.setZoom = function(zoomLevel) {
            const res = getResolution()
            svgcontent.setAttribute(
                'viewBox',
                '0 0 ' + res.w / zoomLevel + ' ' + res.h / zoomLevel
            )
            currentZoom = zoomLevel
            $.each(selectedElements, function(i, elem) {
                if (!elem) {
                    return
                }
                selectorManager.requestSelector(elem).resize()
            })
            pathActions.zoomChange()
            runExtensions(
                'zoomChanged',
                /** @type {module:svgcanvas.SvgCanvas#event:ext_zoomChanged} */ zoomLevel
            )
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getMode
         * @returns {string} The current editor mode string
         */
        this.getMode = function() {
            return currentMode
        }

        /**
         * Sets the editor's mode to the given string.
         * @function module:svgcanvas.SvgCanvas#setMode
         * @param {string} name - String with the new mode to change to
         * @returns {void}
         */
        this.setMode = function(name, attrs = {}) {
            pathActions.clear(true)
            textActions.clear()
            curProperties =
                selectedElements[0] && selectedElements[0].nodeName === 'text'
                    ? curText
                    : curShape
            currentMode = name
            drawAttrs = attrs
        }

        /**
         * Group: Element Styling
         */

        /**
         * @typedef {PlainObject} module:svgcanvas.PaintOptions
         * @property {"solidColor"} type
         */

        /**
         * @function module:svgcanvas.SvgCanvas#getColor
         * @param {string} type
         * @returns {string|module:svgcanvas.PaintOptions|Float|module:jGraduate~Paint} The current fill/stroke option
         */
        this.getColor = function(type) {
            return curProperties[type]
        }

        /**
         * Change the current stroke/fill color/gradient value.
         * @function module:svgcanvas.SvgCanvas#setColor
         * @param {string} type - String indicating fill or stroke
         * @param {string} val - The value to set the stroke attribute to
         * @param {boolean} preventUndo - Boolean indicating whether or not this should be an undoable option
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setColor = function(type, val, preventUndo) {
            curShape[type] = val
            curProperties[type + '_paint'] = { type: 'solidColor' }
            const elems = []
            /**
             *
             * @param {Element} e
             * @returns {void}
             */
            function addNonG(e) {
                if (e.nodeName !== 'g') {
                    elems.push(e)
                }
            }
            let i = selectedElements.length
            while (i--) {
                const elem = selectedElements[i]
                if (elem) {
                    if (elem.tagName === 'g') {
                        walkTree(elem, addNonG)
                    } else if (type === 'fill') {
                        if (
                            elem.tagName !== 'polyline' &&
                            elem.tagName !== 'line'
                        ) {
                            elems.push(elem)
                        }
                    } else {
                        elems.push(elem)
                    }
                }
            }
            if (elems.length > 0) {
                if (!preventUndo) {
                    changeSelectedAttribute(type, val, elems)
                    call('changed', elems)
                } else {
                    changeSelectedAttributeNoUndo(type, val, elems)
                }
            }
        }

        /**
         * Apply the current gradient to selected element's fill or stroke.
         * @function module:svgcanvas.SvgCanvas#setGradient
         * @param {"fill"|"stroke"} type - String indicating "fill" or "stroke" to apply to an element
         * @returns {void}
         */
        const setGradient = (this.setGradient = function(type) {
            if (
                !curProperties[type + '_paint'] ||
                curProperties[type + '_paint'].type === 'solidColor'
            ) {
                return
            }
            let grad = canvas[type + 'Grad']
            // find out if there is a duplicate gradient already in the defs
            const duplicateGrad = findDuplicateGradient(grad)
            const defs = findDefs()
            // no duplicate found, so import gradient into defs
            if (!duplicateGrad) {
                // const origGrad = grad;
                grad = defs.appendChild(svgdoc.importNode(grad, true))
                // get next id and set it on the grad
                grad.id = getNextId()
            } else {
                // use existing gradient
                grad = duplicateGrad
            }
            canvas.setColor(type, 'url(#' + grad.id + ')')
        })

        /**
         * Check if exact gradient already exists.
         * @function module:svgcanvas~findDuplicateGradient
         * @param {SVGGradientElement} grad - The gradient DOM element to compare to others
         * @returns {SVGGradientElement} The existing gradient if found, `null` if not
         */
        const findDuplicateGradient = function(grad) {
            const defs = findDefs()
            const existingGrads = $(defs).find('linearGradient, radialGradient')
            let i = existingGrads.length
            const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy']
            while (i--) {
                const og = existingGrads[i]
                if (grad.tagName === 'linearGradient') {
                    if (
                        grad.getAttribute('x1') !== og.getAttribute('x1') ||
                        grad.getAttribute('y1') !== og.getAttribute('y1') ||
                        grad.getAttribute('x2') !== og.getAttribute('x2') ||
                        grad.getAttribute('y2') !== og.getAttribute('y2')
                    ) {
                        continue
                    }
                } else {
                    const gradAttrs = $(grad).attr(radAttrs)
                    const ogAttrs = $(og).attr(radAttrs)

                    let diff = false
                    $.each(radAttrs, function(j, attr) {
                        if (gradAttrs[attr] !== ogAttrs[attr]) {
                            diff = true
                        }
                    })

                    if (diff) {
                        continue
                    }
                }

                // else could be a duplicate, iterate through stops
                const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop')
                const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop')

                if (stops.length !== ostops.length) {
                    continue
                }

                let j = stops.length
                while (j--) {
                    const stop = stops[j]
                    const ostop = ostops[j]

                    if (
                        stop.getAttribute('offset') !==
                            ostop.getAttribute('offset') ||
                        stop.getAttribute('stop-opacity') !==
                            ostop.getAttribute('stop-opacity') ||
                        stop.getAttribute('stop-color') !==
                            ostop.getAttribute('stop-color')
                    ) {
                        break
                    }
                }

                if (j === -1) {
                    return og
                }
            } // for each gradient in defs

            return null
        }

        /**
         * Set a color/gradient to a fill/stroke.
         * @function module:svgcanvas.SvgCanvas#setPaint
         * @param {"fill"|"stroke"} type - String with "fill" or "stroke"
         * @param {module:jGraduate.jGraduatePaintOptions} paint - The jGraduate paint object to apply
         * @returns {void}
         */
        this.setPaint = function(type, paint) {
            // make a copy
            const p = new $.jGraduate.Paint(paint)
            this.setPaintOpacity(type, p.alpha / 100, true)

            // now set the current paint object
            curProperties[type + '_paint'] = p
            switch (p.type) {
                case 'solidColor':
                    this.setColor(
                        type,
                        p.solidColor !== 'none' ? '#' + p.solidColor : 'none'
                    )
                    break
                case 'linearGradient':
                case 'radialGradient':
                    canvas[type + 'Grad'] = p[p.type]
                    setGradient(type)
                    break
            }
        }

        /**
         * @function module:svgcanvas.SvgCanvas#setStrokePaint
         * @param {module:jGraduate~Paint} paint
         * @returns {void}
         */
        this.setStrokePaint = function(paint) {
            this.setPaint('stroke', paint)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#setFillPaint
         * @param {module:jGraduate~Paint} paint
         * @returns {void}
         */
        this.setFillPaint = function(paint) {
            this.setPaint('fill', paint)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getStrokeWidth
         * @returns {Float|string} The current stroke-width value
         */
        this.getStrokeWidth = function() {
            return curProperties.stroke_width
        }

        /**
         * Sets the stroke width for the current selected elements.
         * When attempting to set a line's width to 0, this changes it to 1 instead.
         * @function module:svgcanvas.SvgCanvas#setStrokeWidth
         * @param {Float} val - A Float indicating the new stroke width value
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setStrokeWidth = function(val) {
            if (val === 0 && ['line', 'path'].includes(currentMode)) {
                canvas.setStrokeWidth(1)
                return
            }
            curProperties.stroke_width = val

            const elems = []
            /**
             *
             * @param {Element} e
             * @returns {void}
             */
            function addNonG(e) {
                if (e.nodeName !== 'g') {
                    elems.push(e)
                }
            }
            let i = selectedElements.length
            while (i--) {
                const elem = selectedElements[i]
                if (elem) {
                    if (elem.tagName === 'g') {
                        walkTree(elem, addNonG)
                    } else {
                        elems.push(elem)
                    }
                }
            }
            if (elems.length > 0) {
                changeSelectedAttribute('stroke-width', val, elems)
                call('changed', selectedElements)
            }
        }

        /**
         * Set the given stroke-related attribute the given value for selected elements.
         * @function module:svgcanvas.SvgCanvas#setStrokeAttr
         * @param {string} attr - String with the attribute name
         * @param {string|Float} val - String or number with the attribute value
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setStrokeAttr = function(attr, val) {
            curShape[attr.replace('-', '_')] = val
            const elems = []

            let i = selectedElements.length
            while (i--) {
                const elem = selectedElements[i]
                if (elem) {
                    if (elem.tagName === 'g') {
                        walkTree(elem, function(e) {
                            if (e.nodeName !== 'g') {
                                elems.push(e)
                            }
                        })
                    } else {
                        elems.push(elem)
                    }
                }
            }
            if (elems.length > 0) {
                changeSelectedAttribute(attr, val, elems)
                call('changed', selectedElements)
            }
        }

        /**
         * @typedef {PlainObject} module:svgcanvas.StyleOptions
         * @property {string} fill
         * @property {Float} fill_opacity
         * @property {string} stroke
         * @property {Float} stroke_width
         * @property {string} stroke_dasharray
         * @property {string} stroke_linejoin
         * @property {string} stroke_linecap
         * @property {Float} stroke_opacity
         * @property {Float} opacity
         */

        /**
         * @function module:svgcanvas.SvgCanvas#getStyle
         * @returns {module:svgcanvas.StyleOptions} current style options
         */
        this.getStyle = function() {
            return curShape
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getOpacity
         * @returns {Float} the current opacity
         */
        this.getOpacity = getOpacity

        /**
         * Sets the given opacity on the current selected elements.
         * @function module:svgcanvas.SvgCanvas#setOpacity
         * @param {string} val
         * @returns {void}
         */
        this.setOpacity = function(val) {
            curShape.opacity = val
            changeSelectedAttribute('opacity', val)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getFillOpacity
         * @returns {Float} the current fill opacity
         */
        this.getFillOpacity = function() {
            return curShape.fill_opacity
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getStrokeOpacity
         * @returns {string} the current stroke opacity
         */
        this.getStrokeOpacity = function() {
            return curShape.stroke_opacity
        }

        /**
         * Sets the current fill/stroke opacity.
         * @function module:svgcanvas.SvgCanvas#setPaintOpacity
         * @param {string} type - String with "fill" or "stroke"
         * @param {Float} val - Float with the new opacity value
         * @param {boolean} preventUndo - Indicates whether or not this should be an undoable action
         * @returns {void}
         */
        this.setPaintOpacity = function(type, val, preventUndo) {
            curShape[type + '_opacity'] = val
            if (!preventUndo) {
                changeSelectedAttribute(type + '-opacity', val)
            } else {
                changeSelectedAttributeNoUndo(type + '-opacity', val)
            }
        }

        /**
         * Gets the current fill/stroke opacity.
         * @function module:svgcanvas.SvgCanvas#getPaintOpacity
         * @param {"fill"|"stroke"} type - String with "fill" or "stroke"
         * @returns {Float} Fill/stroke opacity
         */
        this.getPaintOpacity = function(type) {
            return type === 'fill'
                ? this.getFillOpacity()
                : this.getStrokeOpacity()
        }

        /**
         * Gets the `stdDeviation` blur value of the given element.
         * @function module:svgcanvas.SvgCanvas#getBlur
         * @param {Element} elem - The element to check the blur value for
         * @returns {string} stdDeviation blur attribute value
         */
        this.getBlur = function(elem) {
            let val = 0
            // const elem = selectedElements[0];

            if (elem) {
                const filterUrl = elem.getAttribute('filter')
                if (filterUrl) {
                    const blur = getElem(elem.id + '_blur')
                    if (blur) {
                        val = blur.firstChild.getAttribute('stdDeviation')
                    }
                }
            }
            return val
        }
        ;(function() {
            let curCommand = null
            let filter = null
            let filterHidden = false

            /**
             * Sets the `stdDeviation` blur value on the selected element without being undoable.
             * @function module:svgcanvas.SvgCanvas#setBlurNoUndo
             * @param {Float} val - The new `stdDeviation` value
             * @returns {void}
             */
            canvas.setBlurNoUndo = function(val) {
                if (!filter) {
                    canvas.setBlur(val)
                    return
                }
                if (val === 0) {
                    // Don't change the StdDev, as that will hide the element.
                    // Instead, just remove the value for "filter"
                    changeSelectedAttributeNoUndo('filter', '')
                    filterHidden = true
                } else {
                    const elem = selectedElements[0]
                    if (filterHidden) {
                        changeSelectedAttributeNoUndo(
                            'filter',
                            'url(#' + elem.id + '_blur)'
                        )
                    }
                    if (isWebkit()) {
                        // console.log('e', elem); // eslint-disable-line no-console
                        elem.removeAttribute('filter')
                        elem.setAttribute(
                            'filter',
                            'url(#' + elem.id + '_blur)'
                        )
                    }
                    changeSelectedAttributeNoUndo('stdDeviation', val, [
                        filter.firstChild
                    ])
                    canvas.setBlurOffsets(filter, val)
                }
            }

            /**
             *
             * @returns {void}
             */
            function finishChange() {
                const bCmd = canvas.undoMgr.finishUndoableChange()
                curCommand.addSubCommand(bCmd)
                addCommandToHistory(curCommand)
                curCommand = null
                filter = null
            }

            /**
             * Sets the `x`, `y`, `width`, `height` values of the filter element in order to
             * make the blur not be clipped. Removes them if not neeeded.
             * @function module:svgcanvas.SvgCanvas#setBlurOffsets
             * @param {Element} filterElem - The filter DOM element to update
             * @param {Float} stdDev - The standard deviation value on which to base the offset size
             * @returns {void}
             */
            canvas.setBlurOffsets = function(filterElem, stdDev) {
                if (stdDev > 3) {
                    // TODO: Create algorithm here where size is based on expected blur
                    assignAttributes(
                        filterElem,
                        {
                            x: '-50%',
                            y: '-50%',
                            width: '200%',
                            height: '200%'
                        },
                        100
                    )
                    // Removing these attributes hides text in Chrome (see Issue 579)
                } else if (!isWebkit()) {
                    filterElem.removeAttribute('x')
                    filterElem.removeAttribute('y')
                    filterElem.removeAttribute('width')
                    filterElem.removeAttribute('height')
                }
            }

            /**
             * Adds/updates the blur filter to the selected element.
             * @function module:svgcanvas.SvgCanvas#setBlur
             * @param {Float} val - Float with the new `stdDeviation` blur value
             * @param {boolean} complete - Whether or not the action should be completed (to add to the undo manager)
             * @returns {void}
             */
            canvas.setBlur = function(val, complete) {
                if (curCommand) {
                    finishChange()
                    return
                }

                // Looks for associated blur, creates one if not found
                const elem = selectedElements[0]
                const elemId = elem.id
                filter = getElem(elemId + '_blur')

                val -= 0

                const batchCmd = new BatchCommand()

                // Blur found!
                if (filter) {
                    if (val === 0) {
                        filter = null
                    }
                } else {
                    // Not found, so create
                    const newblur = addSVGElementFromJson({
                        element: 'feGaussianBlur',
                        attr: {
                            in: 'SourceGraphic',
                            stdDeviation: val
                        }
                    })

                    filter = addSVGElementFromJson({
                        element: 'filter',
                        attr: {
                            id: elemId + '_blur'
                        }
                    })

                    filter.append(newblur)
                    findDefs().append(filter)

                    batchCmd.addSubCommand(new InsertElementCommand(filter))
                }

                const changes = { filter: elem.getAttribute('filter') }

                if (val === 0) {
                    elem.removeAttribute('filter')
                    batchCmd.addSubCommand(
                        new ChangeElementCommand(elem, changes)
                    )
                    return
                }

                changeSelectedAttribute('filter', 'url(#' + elemId + '_blur)')
                batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
                canvas.setBlurOffsets(filter, val)

                curCommand = batchCmd
                canvas.undoMgr.beginUndoableChange('stdDeviation', [
                    filter ? filter.firstChild : null
                ])
                if (complete) {
                    canvas.setBlurNoUndo(val)
                    finishChange()
                }
            }
        })()

        /**
         * Check whether selected element is bold or not.
         * @function module:svgcanvas.SvgCanvas#getBold
         * @returns {boolean} Indicates whether or not element is bold
         */
        this.getBold = function() {
            // should only have one element selected
            const selected = selectedElements[0]
            if (
                !isNullish(selected) &&
                selected.tagName === 'text' &&
                isNullish(selectedElements[1])
            ) {
                return selected.getAttribute('font-weight') === 'bold'
            }
            return false
        }

        /**
         * Make the selected element bold or normal.
         * @function module:svgcanvas.SvgCanvas#setBold
         * @param {boolean} b - Indicates bold (`true`) or normal (`false`)
         * @returns {void}
         */
        this.setBold = function(b) {
            const selected = selectedElements[0]
            if (
                !isNullish(selected) &&
                selected.tagName === 'text' &&
                isNullish(selectedElements[1])
            ) {
                changeSelectedAttribute('font-weight', b ? 'bold' : 'normal')
            }
            if (!selectedElements[0].textContent) {
                textActions.setCursor()
            }
        }

        /**
         * Check whether selected element is in italics or not.
         * @function module:svgcanvas.SvgCanvas#getItalic
         * @returns {boolean} Indicates whether or not element is italic
         */
        this.getItalic = function() {
            const selected = selectedElements[0]
            if (
                !isNullish(selected) &&
                selected.tagName === 'text' &&
                isNullish(selectedElements[1])
            ) {
                return selected.getAttribute('font-style') === 'italic'
            }
            return false
        }

        /**
         * Make the selected element italic or normal.
         * @function module:svgcanvas.SvgCanvas#setItalic
         * @param {boolean} i - Indicates italic (`true`) or normal (`false`)
         * @returns {void}
         */
        this.setItalic = function(i) {
            const selected = selectedElements[0]
            if (
                !isNullish(selected) &&
                selected.tagName === 'text' &&
                isNullish(selectedElements[1])
            ) {
                changeSelectedAttribute('font-style', i ? 'italic' : 'normal')
            }
            if (!selectedElements[0].textContent) {
                textActions.setCursor()
            }
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getFontFamily
         * @returns {string} The current font family
         */
        this.getFontFamily = function() {
            return curText.font_family
        }

        /**
         * Set the new font family.
         * @function module:svgcanvas.SvgCanvas#setFontFamily
         * @param {string} val - String with the new font family
         * @returns {void}
         */
        this.setFontFamily = function(val) {
            curText.font_family = val
            changeSelectedAttribute('font-family', val)
            if (selectedElements[0] && !selectedElements[0].textContent) {
                textActions.setCursor()
            }
        }

        /**
         * Set the new font color.
         * @function module:svgcanvas.SvgCanvas#setFontColor
         * @param {string} val - String with the new font color
         * @returns {void}
         */
        this.setFontColor = function(val) {
            curText.fill = val
            changeSelectedAttribute('fill', val)
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getFontColor
         * @returns {string} The current font color
         */
        this.getFontColor = function() {
            return curText.fill
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getFontSize
         * @returns {Float} The current font size
         */
        this.getFontSize = function() {
            return curText.font_size
        }

        /**
         * Applies the given font size to the selected element.
         * @function module:svgcanvas.SvgCanvas#setFontSize
         * @param {Float} val - Float with the new font size
         * @returns {void}
         */
        this.setFontSize = function(val) {
            curText.font_size = val
            changeSelectedAttribute('font-size', val)
            if (!selectedElements[0].textContent) {
                textActions.setCursor()
            }
        }

        /**
         * @function module:svgcanvas.SvgCanvas#getText
         * @returns {string} The current text (`textContent`) of the selected element
         */
        this.getText = function() {
            const selected = selectedElements[0]
            if (isNullish(selected)) {
                return ''
            }
            return selected.textContent
        }

        /**
         * Updates the text element with the given string.
         * @function module:svgcanvas.SvgCanvas#setTextContent
         * @param {string} val - String with the new text
         * @returns {void}
         */
        this.setTextContent = function(val) {
            changeSelectedAttribute('#text', val)
            textActions.init(val)
            textActions.setCursor()
        }

        /**
         * Sets the new image URL for the selected image element. Updates its size if
         * a new URL is given.
         * @function module:svgcanvas.SvgCanvas#setImageURL
         * @param {string} val - String with the image URL/path
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setImageURL = function(val) {
            const elem = selectedElements[0]
            if (!elem) {
                return
            }

            const attrs = $(elem).attr(['width', 'height'])
            const setsize = !attrs.width || !attrs.height

            const curHref = getHref(elem)

            // Do nothing if no URL change or size change
            if (curHref === val && !setsize) {
                return
            }

            const batchCmd = new BatchCommand('Change Image URL')

            setHref(elem, val)
            batchCmd.addSubCommand(
                new ChangeElementCommand(elem, {
                    '#href': curHref
                })
            )

            $(new Image())
                .load(function() {
                    const changes = $(elem).attr(['width', 'height'])

                    $(elem).attr({
                        width: this.width,
                        height: this.height
                    })

                    selectorManager.requestSelector(elem).resize()

                    batchCmd.addSubCommand(
                        new ChangeElementCommand(elem, changes)
                    )
                    addCommandToHistory(batchCmd)
                    call('changed', [elem])
                })
                .attr('src', val)
        }

        /**
         * Sets the new link URL for the selected anchor element.
         * @function module:svgcanvas.SvgCanvas#setLinkURL
         * @param {string} val - String with the link URL/path
         * @returns {void}
         */
        this.setLinkURL = function(val) {
            let elem = selectedElements[0]
            if (!elem) {
                return
            }
            if (elem.tagName !== 'a') {
                // See if parent is an anchor
                const parentsA = $(elem).parents('a')
                if (parentsA.length) {
                    elem = parentsA[0]
                } else {
                    return
                }
            }

            const curHref = getHref(elem)

            if (curHref === val) {
                return
            }

            const batchCmd = new BatchCommand('Change Link URL')

            setHref(elem, val)
            batchCmd.addSubCommand(
                new ChangeElementCommand(elem, {
                    '#href': curHref
                })
            )

            addCommandToHistory(batchCmd)
        }

        /**
         * Sets the `rx` and `ry` values to the selected `rect` element
         * to change its corner radius.
         * @function module:svgcanvas.SvgCanvas#setRectRadius
         * @param {string|Float} val - The new radius
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.setRectRadius = function(val) {
            const selected = selectedElements[0]
            if (!isNullish(selected) && selected.tagName === 'rect') {
                const r = selected.getAttribute('rx')
                if (r !== String(val)) {
                    selected.setAttribute('rx', val)
                    selected.setAttribute('ry', val)
                    addCommandToHistory(
                        new ChangeElementCommand(
                            selected,
                            { rx: r, ry: r },
                            'Radius'
                        )
                    )
                    call('changed', [selected])
                }
            }
        }

        /**
         * Wraps the selected element(s) in an anchor element or converts group to one.
         * @function module:svgcanvas.SvgCanvas#makeHyperlink
         * @param {string} url
         * @returns {void}
         */
        this.makeHyperlink = function(url) {
            canvas.groupSelectedElements('a', url)

            // TODO: If element is a single "g", convert to "a"
            //  if (selectedElements.length > 1 && selectedElements[1]) {
        }

        /**
         * @function module:svgcanvas.SvgCanvas#removeHyperlink
         * @returns {void}
         */
        this.removeHyperlink = function() {
            canvas.ungroupSelectedElement()
        }

        /**
         * Group: Element manipulation
         */

        /**
         * Sets the new segment type to the selected segment(s).
         * @function module:svgcanvas.SvgCanvas#setSegType
         * @param {Integer} newType - New segment type. See {@link https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg} for list
         * @returns {void}
         */
        this.setSegType = function(newType) {
            pathActions.setSegType(newType)
        }

        /**
         * Convert selected element to a path, or get the BBox of an element-as-path.
         * @function module:svgcanvas.SvgCanvas#convertToPath
         * @todo (codedread): Remove the getBBox argument and split this function into two.
         * @param {Element} elem - The DOM element to be converted
         * @param {boolean} getBBox - Boolean on whether or not to only return the path's BBox
         * @returns {void|DOMRect|false|SVGPathElement|null} If the getBBox flag is true, the resulting path's bounding box object.
         * Otherwise the resulting path element is returned.
         */
        this.convertToPath = function(elem, getBBox) {
            if (isNullish(elem)) {
                const elems = selectedElements
                $.each(elems, function(i, el) {
                    if (el) {
                        canvas.convertToPath(el)
                    }
                })
                return undefined
            }
            if (getBBox) {
                return getBBoxOfElementAsPath(
                    elem,
                    addSVGElementFromJson,
                    pathActions
                )
            }
            // TODO: Why is this applying attributes from curShape, then inside utilities.convertToPath it's pulling addition attributes from elem?
            // TODO: If convertToPath is called with one elem, curShape and elem are probably the same; but calling with multiple is a bug or cool feature.
            const attrs = {
                fill: curShape.fill,
                'fill-opacity': curShape.fill_opacity,
                stroke: curShape.stroke,
                'stroke-width': curShape.stroke_width,
                'stroke-dasharray': curShape.stroke_dasharray,
                'stroke-linejoin': curShape.stroke_linejoin,
                'stroke-linecap': curShape.stroke_linecap,
                'stroke-opacity': curShape.stroke_opacity,
                opacity: curShape.opacity,
                visibility: 'hidden'
            }
            return convertToPath(
                elem,
                attrs,
                addSVGElementFromJson,
                pathActions,
                clearSelection,
                addToSelection,
                hstry,
                addCommandToHistory
            )
        }

        /**
         * This function makes the changes to the elements. It does not add the change
         * to the history stack.
         * @param {string} attr - Attribute name
         * @param {string|Float} newValue - String or number with the new attribute value
         * @param {Element[]} elems - The DOM elements to apply the change to
         * @returns {void}
         */
        const changeSelectedAttributeNoUndo = function(attr, newValue, elems) {
            if (currentMode === 'pathedit') {
                // Editing node
                pathActions.moveNode(attr, newValue)
            }
            elems = elems || selectedElements
            let i = elems.length
            const noXYElems = ['g', 'polyline', 'path']
            // const goodGAttrs = ['transform', 'opacity', 'filter'];

            while (i--) {
                let elem = elems[i]
                if (isNullish(elem)) {
                    continue
                }

                // Set x,y vals on elements that don't have them
                if (
                    (attr === 'x' || attr === 'y') &&
                    noXYElems.includes(elem.tagName)
                ) {
                    const bbox = getStrokedBBoxDefaultVisible([elem])
                    const diffX = attr === 'x' ? newValue - bbox.x : 0
                    const diffY = attr === 'y' ? newValue - bbox.y : 0
                    canvas.moveSelectedElements(
                        diffX * currentZoom,
                        diffY * currentZoom,
                        true
                    )
                    continue
                }

                // only allow the transform/opacity/filter attribute to change on <g> elements, slightly hacky
                // TODO: FIXME: Missing statement body
                // if (elem.tagName === 'g' && goodGAttrs.includes(attr)) {}
                let oldval =
                    attr === '#text'
                        ? elem.textContent
                        : elem.getAttribute(attr)
                if (isNullish(oldval)) {
                    oldval = ''
                }
                if (oldval !== String(newValue)) {
                    if (attr === '#text') {
                        // const oldW = utilsGetBBox(elem).width;
                        elem.textContent = newValue

                        // FF bug occurs on on rotated elements
                        if (/rotate/.test(elem.getAttribute('transform'))) {
                            elem = ffClone(elem)
                        }
                        // Hoped to solve the issue of moving text with text-anchor="start",
                        // but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd
                        // const box = getBBox(elem), left = box.x, top = box.y, {width, height} = box,
                        //   dx = width - oldW, dy = 0;
                        // const angle = getRotationAngle(elem, true);
                        // if (angle) {
                        //   const r = Math.sqrt(dx * dx + dy * dy);
                        //   const theta = Math.atan2(dy, dx) - angle;
                        //   dx = r * Math.cos(theta);
                        //   dy = r * Math.sin(theta);
                        //
                        //   elem.setAttribute('x', elem.getAttribute('x') - dx);
                        //   elem.setAttribute('y', elem.getAttribute('y') - dy);
                        // }
                    } else if (attr === '#href') {
                        setHref(elem, newValue)
                    } else {
                        elem.setAttribute(attr, newValue)
                    }

                    // Go into "select" mode for text changes
                    // NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like
                    // font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),
                    // after calling textActions.toSelectMode() below
                    if (
                        currentMode === 'textedit' &&
                        attr !== '#text' &&
                        elem.textContent.length
                    ) {
                        textActions.toSelectMode(elem)
                    }

                    // if (i === 0) {
                    //   selectedBBoxes[0] = utilsGetBBox(elem);
                    // }

                    // Use the Firefox ffClone hack for text elements with gradients or
                    // where other text attributes are changed.
                    if (
                        isGecko() &&
                        elem.nodeName === 'text' &&
                        /rotate/.test(elem.getAttribute('transform'))
                    ) {
                        if (
                            String(newValue).startsWith('url') ||
                            (['font-size', 'font-family', 'x', 'y'].includes(
                                attr
                            ) &&
                                elem.textContent)
                        ) {
                            elem = ffClone(elem)
                        }
                    }
                    // Timeout needed for Opera & Firefox
                    // codedread: it is now possible for this function to be called with elements
                    // that are not in the selectedElements array, we need to only request a
                    // selector if the element is in that array
                    if (selectedElements.includes(elem)) {
                        setTimeout(function() {
                            // Due to element replacement, this element may no longer
                            // be part of the DOM
                            if (!elem.parentNode) {
                                return
                            }
                            selectorManager.requestSelector(elem).resize()
                        }, 0)
                    }
                    // if this element was rotated, and we changed the position of this element
                    // we need to update the rotational transform attribute
                    const angle = getRotationAngle(elem)
                    if (angle !== 0 && attr !== 'transform') {
                        const tlist = getTransformList(elem)
                        let n = tlist.numberOfItems
                        while (n--) {
                            const xform = tlist.getItem(n)
                            if (xform.type === 4) {
                                // remove old rotate
                                tlist.removeItem(n)

                                const box = utilsGetBBox(elem)
                                const center = transformPoint(
                                    box.x + box.width / 2,
                                    box.y + box.height / 2,
                                    transformListToTransform(tlist).matrix
                                )
                                const cx = center.x,
                                    cy = center.y
                                const newrot = svgroot.createSVGTransform()
                                newrot.setRotate(angle, cx, cy)
                                tlist.insertItemBefore(newrot, n)
                                break
                            }
                        }
                    }
                } // if oldValue != newValue
            } // for each elem
        }

        /**
         * Change the given/selected element and add the original value to the history stack.
         * If you want to change all `selectedElements`, ignore the `elems` argument.
         * If you want to change only a subset of `selectedElements`, then send the
         * subset to this function in the `elems` argument.
         * @function module:svgcanvas.SvgCanvas#changeSelectedAttribute
         * @param {string} attr - String with the attribute name
         * @param {string|Float} val - String or number with the new attribute value
         * @param {Element[]} elems - The DOM elements to apply the change to
         * @returns {void}
         */
        const changeSelectedAttribute = (this.changeSelectedAttribute = function(
            attr,
            val,
            elems
        ) {
            elems = elems || selectedElements
            canvas.undoMgr.beginUndoableChange(attr, elems)
            // const i = elems.length;

            changeSelectedAttributeNoUndo(attr, val, elems)

            const batchCmd = canvas.undoMgr.finishUndoableChange()
            if (!batchCmd.isEmpty()) {
                addCommandToHistory(batchCmd)
            }
        })

        /**
         * Removes all selected elements from the DOM and adds the change to the
         * history stack.
         * @function module:svgcanvas.SvgCanvas#deleteSelectedElements
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.deleteSelectedElements = function() {
            const batchCmd = new BatchCommand('Delete Elements')
            const len = selectedElements.length
            const selectedCopy = [] // selectedElements is being deleted

            for (let i = 0; i < len; ++i) {
                const selected = selectedElements[i]
                if (isNullish(selected)) {
                    break
                }

                let parent = selected.parentNode
                let t = selected

                // this will unselect the element and remove the selectedOutline
                selectorManager.releaseSelector(t)

                // Remove the path if present.
                pathModule.removePath_(t.id)

                // Get the parent if it's a single-child anchor
                if (parent.tagName === 'a' && parent.childNodes.length === 1) {
                    t = parent
                    parent = parent.parentNode
                }

                const { nextSibling } = t
                const elem = parent.removeChild(t)
                selectedCopy.push(selected) // for the copy
                batchCmd.addSubCommand(
                    new RemoveElementCommand(elem, nextSibling, parent)
                )
            }
            selectedElements = []

            if (!batchCmd.isEmpty()) {
                addCommandToHistory(batchCmd)
            }
            call('changed', selectedCopy)
            clearSelection()
        }

        /**
         * Removes all selected elements from the DOM and adds the change to the
         * history stack. Remembers removed elements on the clipboard.
         * @function module:svgcanvas.SvgCanvas#cutSelectedElements
         * @returns {void}
         */
        this.cutSelectedElements = function() {
            canvas.copySelectedElements()
            canvas.deleteSelectedElements()
        }

        /**
         * Remembers the current selected elements on the clipboard.
         * @function module:svgcanvas.SvgCanvas#copySelectedElements
         * @returns {void}
         */
        this.copySelectedElements = function() {
            let length = selectedElements.length
            let arr = []
            if (length > 1) {
                arr = selectedElements.filter(item => {
                    return !item.getAttribute('room_parent_id')
                })
            } else {
                arr = selectedElements
            }
            localStorage.setItem(
                'svgedit_clipboard',
                JSON.stringify(
                    arr.map(function(x) {
                        return getJsonFromSvgElement(x)
                    })
                )
            )

            $('#cmenu_canvas').enableContextMenuItems('#paste,#paste_in_place')
        }

        /**
         * @function module:svgcanvas.SvgCanvas#pasteElements
         * @param {"in_place"|"point"|void} type
         * @param {Integer|void} x Expected if type is "point"
         * @param {Integer|void} y Expected if type is "point"
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @fires module:svgcanvas.SvgCanvas#event:ext_IDsUpdated
         * @returns {void}
         */
        this.pasteElements = function(type, x, y) {
            let clipb = JSON.parse(localStorage.getItem('svgedit_clipboard'))
            let len = clipb.length
            if (!len) {
                return
            }

            const pasted = []
            const batchCmd = new BatchCommand('Paste elements')
            // const drawing = getCurrentDrawing();
            /**
             * @typedef {PlainObject<string, string>} module:svgcanvas.ChangedIDs
             */
            /**
             * @type {module:svgcanvas.ChangedIDs}
             */
            const changedIDs = {}

            // Recursively replace IDs and record the changes
            /**
             *
             * @param {module:svgcanvas.SVGAsJSON} elem
             * @returns {void}
             */
            function checkIDs(elem) {
                if (elem.attr && elem.attr.id) {
                    changedIDs[elem.attr.id] = getNextId()
                    elem.attr.id = changedIDs[elem.attr.id]
                }
                if (elem.children) elem.children.forEach(checkIDs)
            }
            clipb.forEach(checkIDs)

            // Give extensions like the connector extension a chance to reflect new IDs and remove invalid elements
            /**
             * Triggered when `pasteElements` is called from a paste action (context menu or key)
             * @event module:svgcanvas.SvgCanvas#event:ext_IDsUpdated
             * @type {PlainObject}
             * @property {module:svgcanvas.SVGAsJSON[]} elems
             * @property {module:svgcanvas.ChangedIDs} changes Maps past ID (on attribute) to current ID
             */
            runExtensions(
                'IDsUpdated',
                /** @type {module:svgcanvas.SvgCanvas#event:ext_IDsUpdated} */
                { elems: clipb, changes: changedIDs },
                true
            ).forEach(function(extChanges) {
                if (!extChanges || !('remove' in extChanges)) return

                extChanges.remove.forEach(function(removeID) {
                    clipb = clipb.filter(function(clipBoardItem) {
                        return clipBoardItem.attr.id !== removeID
                    })
                })
            })

            // Move elements to lastClickPoint
            while (len--) {
                const elem = clipb[len]
                if (!elem) {
                    continue
                }

                const copy = addSVGElementFromJson(elem)
                pasted.push(copy)
                batchCmd.addSubCommand(new InsertElementCommand(copy))

                restoreRefElems(copy)
            }

            selectOnly(pasted)

            if (type !== 'in_place') {
                let ctrX, ctrY

                if (!type) {
                    ctrX = lastClickPoint.x
                    ctrY = lastClickPoint.y
                } else if (type === 'point') {
                    ctrX = x
                    ctrY = y
                }

                const bbox = getStrokedBBoxDefaultVisible(pasted)
                const cx = ctrX - (bbox.x + bbox.width / 2),
                    cy = ctrY - (bbox.y + bbox.height / 2),
                    dx = [],
                    dy = []

                $.each(pasted, function(i, item) {
                    dx.push(cx)
                    dy.push(cy)
                })

                const cmd = canvas.moveSelectedElements(dx, dy, false)
                if (cmd) batchCmd.addSubCommand(cmd)
            }

            addCommandToHistory(batchCmd)
            call('changed', pasted)
        }

        /**
         * Wraps all the selected elements in a group (`g`) element.
         * @function module:svgcanvas.SvgCanvas#groupSelectedElements
         * @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
         * @param {string} [urlArg]
         * @returns {void}
         */
        this.groupSelectedElements = function(type, urlArg) {
            if (!type) {
                type = 'g'
            }
            let cmdStr = ''
            let url

            switch (type) {
                case 'a': {
                    cmdStr = 'Make hyperlink'
                    url = urlArg || ''
                    break
                }
                default: {
                    type = 'g'
                    cmdStr = 'Group Elements'
                    break
                }
            }

            const batchCmd = new BatchCommand(cmdStr)

            // create and insert the group element
            const g = addSVGElementFromJson({
                element: type,
                attr: {
                    id: getNextId()
                }
            })
            if (type === 'a') {
                setHref(g, url)
            }
            batchCmd.addSubCommand(new InsertElementCommand(g))

            // now move all children into the group
            let i = selectedElements.length
            while (i--) {
                let elem = selectedElements[i]
                if (isNullish(elem)) {
                    continue
                }

                if (
                    elem.parentNode.tagName === 'a' &&
                    elem.parentNode.childNodes.length === 1
                ) {
                    elem = elem.parentNode
                }

                const oldNextSibling = elem.nextSibling
                const oldParent = elem.parentNode
                g.append(elem)
                batchCmd.addSubCommand(
                    new MoveElementCommand(elem, oldNextSibling, oldParent)
                )
            }
            if (!batchCmd.isEmpty()) {
                addCommandToHistory(batchCmd)
            }

            // update selection
            selectOnly([g], true)
        }

        /**
         * Pushes all appropriate parent group properties down to its children, then
         * removes them from the group.
         * @function module:svgcanvas.SvgCanvas#pushGroupProperties
         * @param {SVGAElement|SVGGElement} g
         * @param {boolean} undoable
         * @returns {BatchCommand|void}
         */
        const pushGroupProperties = (this.pushGroupProperties = function(
            g,
            undoable
        ) {
            const children = g.childNodes
            const len = children.length
            const xform = g.getAttribute('transform')

            const glist = getTransformList(g)
            const m = transformListToTransform(glist).matrix

            const batchCmd = new BatchCommand('Push group properties')

            // TODO: get all fill/stroke properties from the group that we are about to destroy
            // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
            // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
            // "stroke-width"
            // and then for each child, if they do not have the attribute (or the value is 'inherit')
            // then set the child's attribute

            const gangle = getRotationAngle(g)

            const gattrs = $(g).attr(['filter', 'opacity'])
            let gfilter, gblur, changes
            const drawing = getCurrentDrawing()

            for (let i = 0; i < len; i++) {
                const elem = children[i]

                if (elem.nodeType !== 1) {
                    continue
                }

                if (gattrs.opacity !== null && gattrs.opacity !== 1) {
                    // const c_opac = elem.getAttribute('opacity') || 1;
                    const newOpac =
                        Math.round(
                            (elem.getAttribute('opacity') || 1) *
                                gattrs.opacity *
                                100
                        ) / 100
                    changeSelectedAttribute('opacity', newOpac, [elem])
                }

                if (gattrs.filter) {
                    let cblur = this.getBlur(elem)
                    const origCblur = cblur
                    if (!gblur) {
                        gblur = this.getBlur(g)
                    }
                    if (cblur) {
                        // Is this formula correct?
                        cblur = Number(gblur) + Number(cblur)
                    } else if (cblur === 0) {
                        cblur = gblur
                    }

                    // If child has no current filter, get group's filter or clone it.
                    if (!origCblur) {
                        // Set group's filter to use first child's ID
                        if (!gfilter) {
                            gfilter = getRefElem(gattrs.filter)
                        } else {
                            // Clone the group's filter
                            gfilter = drawing.copyElem(gfilter)
                            findDefs().append(gfilter)
                        }
                    } else {
                        gfilter = getRefElem(elem.getAttribute('filter'))
                    }

                    // Change this in future for different filters
                    const suffix =
                        gfilter.firstChild.tagName === 'feGaussianBlur'
                            ? 'blur'
                            : 'filter'
                    gfilter.id = elem.id + '_' + suffix
                    changeSelectedAttribute(
                        'filter',
                        'url(#' + gfilter.id + ')',
                        [elem]
                    )

                    // Update blur value
                    if (cblur) {
                        changeSelectedAttribute('stdDeviation', cblur, [
                            gfilter.firstChild
                        ])
                        canvas.setBlurOffsets(gfilter, cblur)
                    }
                }

                let chtlist = getTransformList(elem)

                // Don't process gradient transforms
                if (elem.tagName.includes('Gradient')) {
                    chtlist = null
                }

                // Hopefully not a problem to add this. Necessary for elements like <desc/>
                if (!chtlist) {
                    continue
                }

                // Apparently <defs> can get get a transformlist, but we don't want it to have one!
                if (elem.tagName === 'defs') {
                    continue
                }

                if (glist.numberOfItems) {
                    // TODO: if the group's transform is just a rotate, we can always transfer the
                    // rotate() down to the children (collapsing consecutive rotates and factoring
                    // out any translates)
                    if (gangle && glist.numberOfItems === 1) {
                        // [Rg] [Rc] [Mc]
                        // we want [Tr] [Rc2] [Mc] where:
                        //  - [Rc2] is at the child's current center but has the
                        // sum of the group and child's rotation angles
                        //  - [Tr] is the equivalent translation that this child
                        // undergoes if the group wasn't there

                        // [Tr] = [Rg] [Rc] [Rc2_inv]

                        // get group's rotation matrix (Rg)
                        const rgm = glist.getItem(0).matrix

                        // get child's rotation matrix (Rc)
                        let rcm = svgroot.createSVGMatrix()
                        const cangle = getRotationAngle(elem)
                        if (cangle) {
                            rcm = chtlist.getItem(0).matrix
                        }

                        // get child's old center of rotation
                        const cbox = utilsGetBBox(elem)
                        const ceqm = transformListToTransform(chtlist).matrix
                        const coldc = transformPoint(
                            cbox.x + cbox.width / 2,
                            cbox.y + cbox.height / 2,
                            ceqm
                        )

                        // sum group and child's angles
                        const sangle = gangle + cangle

                        // get child's rotation at the old center (Rc2_inv)
                        const r2 = svgroot.createSVGTransform()
                        r2.setRotate(sangle, coldc.x, coldc.y)

                        // calculate equivalent translate
                        const trm = matrixMultiply(
                            rgm,
                            rcm,
                            r2.matrix.inverse()
                        )

                        // set up tlist
                        if (cangle) {
                            chtlist.removeItem(0)
                        }

                        if (sangle) {
                            if (chtlist.numberOfItems) {
                                chtlist.insertItemBefore(r2, 0)
                            } else {
                                chtlist.appendItem(r2)
                            }
                        }

                        if (trm.e || trm.f) {
                            const tr = svgroot.createSVGTransform()
                            tr.setTranslate(trm.e, trm.f)
                            if (chtlist.numberOfItems) {
                                chtlist.insertItemBefore(tr, 0)
                            } else {
                                chtlist.appendItem(tr)
                            }
                        }
                    } else {
                        // more complicated than just a rotate
                        // transfer the group's transform down to each child and then
                        // call recalculateDimensions()
                        const oldxform = elem.getAttribute('transform')
                        changes = {}
                        changes.transform = oldxform || ''

                        const newxform = svgroot.createSVGTransform()

                        // [ gm ] [ chm ] = [ chm ] [ gm' ]
                        // [ gm' ] = [ chmInv ] [ gm ] [ chm ]
                        const chm = transformListToTransform(chtlist).matrix,
                            chmInv = chm.inverse()
                        const gm = matrixMultiply(chmInv, m, chm)
                        newxform.setMatrix(gm)
                        chtlist.appendItem(newxform)
                    }
                    const cmd = recalculateDimensions(elem)
                    if (cmd) {
                        batchCmd.addSubCommand(cmd)
                    }
                }
            }

            // remove transform and make it undo-able
            if (xform) {
                changes = {}
                changes.transform = xform
                g.setAttribute('transform', '')
                g.removeAttribute('transform')
                batchCmd.addSubCommand(new ChangeElementCommand(g, changes))
            }

            if (undoable && !batchCmd.isEmpty()) {
                return batchCmd
            }
            return undefined
        })

        /**
         * Unwraps all the elements in a selected group (`g`) element. This requires
         * significant recalculations to apply group's transforms, etc. to its children.
         * @function module:svgcanvas.SvgCanvas#ungroupSelectedElement
         * @returns {void}
         */
        this.ungroupSelectedElement = function() {
            let g = selectedElements[0]
            if (!g) {
                return
            }
            if ($(g).data('gsvg') || $(g).data('symbol')) {
                // Is svg, so actually convert to group
                convertToGroup(g)
                return
            }
            if (g.tagName === 'use') {
                // Somehow doesn't have data set, so retrieve
                const symbol = getElem(getHref(g).substr(1))
                $(g)
                    .data('symbol', symbol)
                    .data('ref', symbol)
                convertToGroup(g)
                return
            }
            const parentsA = $(g).parents('a')
            if (parentsA.length) {
                g = parentsA[0]
            }

            // Look for parent "a"
            if (g.tagName === 'g' || g.tagName === 'a') {
                const batchCmd = new BatchCommand('Ungroup Elements')
                const cmd = pushGroupProperties(g, true)
                if (cmd) {
                    batchCmd.addSubCommand(cmd)
                }

                const parent = g.parentNode
                const anchor = g.nextSibling
                const children = new Array(g.childNodes.length)

                let i = 0
                while (g.firstChild) {
                    let elem = g.firstChild
                    const oldNextSibling = elem.nextSibling
                    const oldParent = elem.parentNode

                    // Remove child title elements
                    if (elem.tagName === 'title') {
                        const { nextSibling } = elem
                        batchCmd.addSubCommand(
                            new RemoveElementCommand(
                                elem,
                                nextSibling,
                                oldParent
                            )
                        )
                        elem.remove()
                        continue
                    }

                    children[i++] = elem = parent.insertBefore(elem, anchor)
                    batchCmd.addSubCommand(
                        new MoveElementCommand(elem, oldNextSibling, oldParent)
                    )
                }

                // remove the group from the selection
                clearSelection()

                // delete the group element (but make undo-able)
                const gNextSibling = g.nextSibling
                g = parent.removeChild(g)
                batchCmd.addSubCommand(
                    new RemoveElementCommand(g, gNextSibling, parent)
                )

                if (!batchCmd.isEmpty()) {
                    addCommandToHistory(batchCmd)
                }

                // update selection
                addToSelection(children)
            }
        }

        /**
         * Repositions the selected element to the bottom in the DOM to appear on top of
         * other elements.
         * @function module:svgcanvas.SvgCanvas#moveToTopSelectedElement
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.moveToTopSelectedElement = function() {
            const [selected] = selectedElements
            if (!isNullish(selected)) {
                let t = selected
                const oldParent = t.parentNode
                const oldNextSibling = t.nextSibling
                t = t.parentNode.appendChild(t)
                // If the element actually moved position, add the command and fire the changed
                // event handler.
                if (oldNextSibling !== t.nextSibling) {
                    addCommandToHistory(
                        new MoveElementCommand(
                            t,
                            oldNextSibling,
                            oldParent,
                            'top'
                        )
                    )
                    call('changed', [t])
                }
            }
        }

        /**
         * Repositions the selected element to the top in the DOM to appear under
         * other elements.
         * @function module:svgcanvas.SvgCanvas#moveToBottomSelectedElement
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.moveToBottomSelectedElement = function() {
            const [selected] = selectedElements
            if (!isNullish(selected)) {
                let t = selected
                const oldParent = t.parentNode
                const oldNextSibling = t.nextSibling
                let { firstChild } = t.parentNode
                if (firstChild.tagName === 'title') {
                    firstChild = firstChild.nextSibling
                }
                // This can probably be removed, as the defs should not ever apppear
                // inside a layer group
                if (firstChild.tagName === 'defs') {
                    firstChild = firstChild.nextSibling
                }
                t = t.parentNode.insertBefore(t, firstChild)
                // If the element actually moved position, add the command and fire the changed
                // event handler.
                if (oldNextSibling !== t.nextSibling) {
                    addCommandToHistory(
                        new MoveElementCommand(
                            t,
                            oldNextSibling,
                            oldParent,
                            'bottom'
                        )
                    )
                    call('changed', [t])
                }
            }
        }

        /**
         * Moves the select element up or down the stack, based on the visibly
         * intersecting elements.
         * @function module:svgcanvas.SvgCanvas#moveUpDownSelected
         * @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {void}
         */
        this.moveUpDownSelected = function(dir) {
            const selected = selectedElements[0]
            if (!selected) {
                return
            }

            curBBoxes = []
            let closest, foundCur
            // jQuery sorts this list
            const list = $(
                getIntersectionList(getStrokedBBoxDefaultVisible([selected]))
            ).toArray()
            if (dir === 'Down') {
                list.reverse()
            }

            $.each(list, function() {
                if (!foundCur) {
                    if (this === selected) {
                        foundCur = true
                    }
                    return true
                }
                closest = this // eslint-disable-line consistent-this
                return false
            })
            if (!closest) {
                return
            }

            const t = selected
            const oldParent = t.parentNode
            const oldNextSibling = t.nextSibling
            $(closest)[dir === 'Down' ? 'before' : 'after'](t)
            // If the element actually moved position, add the command and fire the changed
            // event handler.
            if (oldNextSibling !== t.nextSibling) {
                addCommandToHistory(
                    new MoveElementCommand(
                        t,
                        oldNextSibling,
                        oldParent,
                        'Move ' + dir
                    )
                )
                call('changed', [t])
            }
        }

        /**
         * Moves selected elements on the X/Y axis.
         * @function module:svgcanvas.SvgCanvas#moveSelectedElements
         * @param {Float} dx - Float with the distance to move on the x-axis
         * @param {Float} dy - Float with the distance to move on the y-axis
         * @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
         * @fires module:svgcanvas.SvgCanvas#event:changed
         * @returns {BatchCommand|void} Batch command for the move
         */
        this.moveSelectedElements = function(dx, dy, undoable) {
            // if undoable is not sent, default to true
            // if single values, scale them to the zoom
            if (dx.constructor !== Array) {
                dx /= currentZoom
                dy /= currentZoom
            }
            undoable = undoable || true
            const batchCmd = new BatchCommand('position')
            let i = selectedElements.length
            while (i--) {
                const selected = selectedElements[i]
                if (!isNullish(selected)) {
                    // if (i === 0) {
                    //   selectedBBoxes[0] = utilsGetBBox(selected);
                    // }
                    // const b = {};
                    // for (const j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j];
                    // selectedBBoxes[i] = b;

                    const xform = svgroot.createSVGTransform()
                    const tlist = getTransformList(selected)

                    // dx and dy could be arrays
                    if (dx.constructor === Array) {
                        // if (i === 0) {
                        //   selectedBBoxes[0].x += dx[0];
                        //   selectedBBoxes[0].y += dy[0];
                        // }
                        xform.setTranslate(dx[i], dy[i])
                    } else {
                        // if (i === 0) {
                        //   selectedBBoxes[0].x += dx;
                        //   selectedBBoxes[0].y += dy;
                        // }
                        xform.setTranslate(dx, dy)
                    }

                    if (tlist.numberOfItems) {
                        tlist.insertItemBefore(xform, 0)
                    } else {
                        tlist.appendItem(xform)
                    }

                    const cmd = recalculateDimensions(selected)
                    if (cmd) {
                        batchCmd.addSubCommand(cmd)
                    }

                    selectorManager.requestSelector(selected).resize()
                }
            }
            if (!batchCmd.isEmpty()) {
                if (undoable) {
                    addCommandToHistory(batchCmd)
                }
                call('changed', selectedElements)
                return batchCmd
            }
            return undefined
        }

        /**
         * Create deep DOM copies (clones) of all selected elements and move them slightly
         * from their originals.
         * @function module:svgcanvas.SvgCanvas#cloneSelectedElements
         * @param {Float} x Float with the distance to move on the x-axis
         * @param {Float} y Float with the distance to move on the y-axis
         * @returns {void}
         */
        this.cloneSelectedElements = function(x, y) {
            let i, elem
            const batchCmd = new BatchCommand('Clone Elements')
            // find all the elements selected (stop at first null)
            const len = selectedElements.length
            /**
             * Sorts an array numerically and ascending.
             * @param {Element} a
             * @param {Element} b
             * @returns {Integer}
             */
            function sortfunction(a, b) {
                return $(b).index() - $(a).index()
            }
            selectedElements.sort(sortfunction)
            for (i = 0; i < len; ++i) {
                elem = selectedElements[i]
                if (isNullish(elem)) {
                    break
                }
            }
            // use slice to quickly get the subset of elements we need
            const copiedElements = selectedElements.slice(0, i)
            this.clearSelection(true)
            // note that we loop in the reverse way because of the way elements are added
            // to the selectedElements array (top-first)
            const drawing = getCurrentDrawing()
            i = copiedElements.length
            while (i--) {
                // clone each element and replace it within copiedElements
                elem = copiedElements[i] = drawing.copyElem(copiedElements[i])
                ;(currentGroup || drawing.getCurrentLayer()).append(elem)
                batchCmd.addSubCommand(new InsertElementCommand(elem))
            }

            if (!batchCmd.isEmpty()) {
                addToSelection(copiedElements.reverse()) // Need to reverse for correct selection-adding
                this.moveSelectedElements(x, y, false)
                addCommandToHistory(batchCmd)
            }
        }

        /**
         * Aligns selected elements.
         * @function module:svgcanvas.SvgCanvas#alignSelectedElements
         * @param {string} type - String with single character indicating the alignment type
         * @param {"selected"|"largest"|"smallest"|"page"} relativeTo
         * @returns {void}
         */
        this.alignSelectedElements = function(type, relativeTo) {
            const bboxes = [] // angles = [];
            const len = selectedElements.length
            if (!len) {
                return
            }
            let minx = Number.MAX_VALUE,
                maxx = Number.MIN_VALUE,
                miny = Number.MAX_VALUE,
                maxy = Number.MIN_VALUE
            let curwidth = Number.MIN_VALUE,
                curheight = Number.MIN_VALUE
            for (let i = 0; i < len; ++i) {
                if (isNullish(selectedElements[i])) {
                    break
                }
                const elem = selectedElements[i]
                bboxes[i] = getStrokedBBoxDefaultVisible([elem])

                // now bbox is axis-aligned and handles rotation
                switch (relativeTo) {
                    case 'smallest':
                        if (
                            ((type === 'l' || type === 'c' || type === 'r') &&
                                (curwidth === Number.MIN_VALUE ||
                                    curwidth > bboxes[i].width)) ||
                            ((type === 't' || type === 'm' || type === 'b') &&
                                (curheight === Number.MIN_VALUE ||
                                    curheight > bboxes[i].height))
                        ) {
                            minx = bboxes[i].x
                            miny = bboxes[i].y
                            maxx = bboxes[i].x + bboxes[i].width
                            maxy = bboxes[i].y + bboxes[i].height
                            curwidth = bboxes[i].width
                            curheight = bboxes[i].height
                        }
                        break
                    case 'largest':
                        if (
                            ((type === 'l' || type === 'c' || type === 'r') &&
                                (curwidth === Number.MIN_VALUE ||
                                    curwidth < bboxes[i].width)) ||
                            ((type === 't' || type === 'm' || type === 'b') &&
                                (curheight === Number.MIN_VALUE ||
                                    curheight < bboxes[i].height))
                        ) {
                            minx = bboxes[i].x
                            miny = bboxes[i].y
                            maxx = bboxes[i].x + bboxes[i].width
                            maxy = bboxes[i].y + bboxes[i].height
                            curwidth = bboxes[i].width
                            curheight = bboxes[i].height
                        }
                        break
                    default:
                        // 'selected'
                        if (bboxes[i].x < minx) {
                            minx = bboxes[i].x
                        }
                        if (bboxes[i].y < miny) {
                            miny = bboxes[i].y
                        }
                        if (bboxes[i].x + bboxes[i].width > maxx) {
                            maxx = bboxes[i].x + bboxes[i].width
                        }
                        if (bboxes[i].y + bboxes[i].height > maxy) {
                            maxy = bboxes[i].y + bboxes[i].height
                        }
                        break
                }
            } // loop for each element to find the bbox and adjust min/max

            if (relativeTo === 'page') {
                minx = 0
                miny = 0
                maxx = canvas.contentW
                maxy = canvas.contentH
            }

            const dx = new Array(len)
            const dy = new Array(len)
            for (let i = 0; i < len; ++i) {
                if (isNullish(selectedElements[i])) {
                    break
                }
                // const elem = selectedElements[i];
                const bbox = bboxes[i]
                dx[i] = 0
                dy[i] = 0
                switch (type) {
                    case 'l': // left (horizontal)
                        dx[i] = minx - bbox.x
                        break
                    case 'c': // center (horizontal)
                        dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2)
                        break
                    case 'r': // right (horizontal)
                        dx[i] = maxx - (bbox.x + bbox.width)
                        break
                    case 't': // top (vertical)
                        dy[i] = miny - bbox.y
                        break
                    case 'm': // middle (vertical)
                        dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2)
                        break
                    case 'b': // bottom (vertical)
                        dy[i] = maxy - (bbox.y + bbox.height)
                        break
                }
            }
            this.moveSelectedElements(dx, dy)
        }

        /**
         * Group: Additional editor tools
         */

        /**
         * @name module:svgcanvas.SvgCanvas#contentW
         * @type {Float}
         */
        this.contentW = getResolution().w
        /**
         * @name module:svgcanvas.SvgCanvas#contentH
         * @type {Float}
         */
        this.contentH = getResolution().h

        /**
         * @typedef {PlainObject} module:svgcanvas.CanvasInfo
         * @property {Float} x - The canvas' new x coordinate
         * @property {Float} y - The canvas' new y coordinate
         * @property {string} oldX - The canvas' old x coordinate
         * @property {string} oldY - The canvas' old y coordinate
         * @property {Float} d_x - The x position difference
         * @property {Float} d_y - The y position difference
         */

        /**
         * Updates the editor canvas width/height/position after a zoom has occurred.
         * @function module:svgcanvas.SvgCanvas#updateCanvas
         * @param {Float} w - Float with the new width
         * @param {Float} h - Float with the new height
         * @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
         * @returns {module:svgcanvas.CanvasInfo}
         */
        this.updateCanvas = function(w, h) {
            svgroot.setAttribute('width', w)
            svgroot.setAttribute('height', h)
            const bg = $('#canvasBackground')[0]
            const oldX = svgcontent.getAttribute('x')
            const oldY = svgcontent.getAttribute('y')
            const x = (w - this.contentW * currentZoom) / 2
            const y = (h - this.contentH * currentZoom) / 2

            assignAttributes(svgcontent, {
                width: this.contentW * currentZoom,
                height: this.contentH * currentZoom,
                x,
                y,
                viewBox: '0 0 ' + this.contentW + ' ' + this.contentH
            })

            assignAttributes(bg, {
                width: svgcontent.getAttribute('width'),
                height: svgcontent.getAttribute('height'),
                x,
                y
            })

            const bgImg = getElem('background_image')
            if (bgImg) {
                assignAttributes(bgImg, {
                    width: '100%',
                    height: '100%'
                })
            }

            selectorManager.selectorParentGroup.setAttribute(
                'transform',
                'translate(' + x + ',' + y + ')'
            )

            /**
             * Invoked upon updates to the canvas.
             * @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
             * @type {PlainObject}
             * @property {Integer} new_x
             * @property {Integer} new_y
             * @property {string} old_x (Of Integer)
             * @property {string} old_y (Of Integer)
             * @property {Integer} d_x
             * @property {Integer} d_y
             */
            runExtensions(
                'canvasUpdated',
                /**
                 * @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
                 */
                {
                    new_x: x,
                    new_y: y,
                    old_x: oldX,
                    old_y: oldY,
                    d_x: x - oldX,
                    d_y: y - oldY
                }
            )
            return {
                x,
                y,
                old_x: oldX,
                old_y: oldY,
                d_x: x - oldX,
                d_y: y - oldY
            }
        }

        /**
         * Set the background of the editor (NOT the actual document).
         * @function module:svgcanvas.SvgCanvas#setBackground
         * @param {string} color - String with fill color to apply
         * @param {string} url - URL or path to image to use
         * @returns {void}
         */
        this.setBackground = function(color, url) {
            const bg = getElem('canvasBackground')
            const border = $(bg).find('rect')[0]
            let bgImg = getElem('background_image')
            border.setAttribute('fill', color)
            border.setAttribute('stroke', '#e4e4e4')
            if (url) {
                if (!bgImg) {
                    bgImg = svgdoc.createElementNS(NS.SVG, 'image')
                    assignAttributes(bgImg, {
                        id: 'background_image',
                        width: '100%',
                        height: '100%',
                        preserveAspectRatio: 'xMinYMin',
                        style: 'pointer-events:none'
                    })
                }
                setHref(bgImg, url)
                bg.append(bgImg)
            } else if (bgImg) {
                bgImg.remove()
            }
        }

        /**
         * Select the next/previous element within the current layer.
         * @function module:svgcanvas.SvgCanvas#cycleElement
         * @param {boolean} next - true = next and false = previous element
         * @fires module:svgcanvas.SvgCanvas#event:selected
         * @returns {void}
         */
        this.cycleElement = function(next) {
            let num
            const curElem = selectedElements[0]
            let elem = false
            const allElems = getVisibleElements(
                currentGroup || getCurrentDrawing().getCurrentLayer()
            )
            if (!allElems.length) {
                return
            }
            if (isNullish(curElem)) {
                num = next ? allElems.length - 1 : 0
                elem = allElems[num]
            } else {
                let i = allElems.length
                while (i--) {
                    if (allElems[i] === curElem) {
                        num = next ? i - 1 : i + 1
                        if (num >= allElems.length) {
                            num = 0
                        } else if (num < 0) {
                            num = allElems.length - 1
                        }
                        elem = allElems[num]
                        break
                    }
                }
            }
            selectOnly([elem], true)
            call('selected', selectedElements)
        }

        this.clear()

        /**
         * @interface module:svgcanvas.PrivateMethods
         * @type {PlainObject}
         * @property {module:svgcanvas~addCommandToHistory} addCommandToHistory
         * @property {module:history.HistoryCommand} BatchCommand
         * @property {module:history.HistoryCommand} ChangeElementCommand
         * @property {module:utilities.decode64} decode64
         * @property {module:utilities.dropXMLInteralSubset} dropXMLInteralSubset
         * @property {module:utilities.encode64} encode64
         * @property {module:svgcanvas~ffClone} ffClone
         * @property {module:svgcanvas~findDuplicateGradient} findDuplicateGradient
         * @property {module:utilities.getPathBBox} getPathBBox
         * @property {module:units.getTypeMap} getTypeMap
         * @property {module:draw.identifyLayers} identifyLayers
         * @property {module:history.HistoryCommand} InsertElementCommand
         * @property {module:browser.isChrome} isChrome
         * @property {module:math.isIdentity} isIdentity
         * @property {module:browser.isIE} isIE
         * @property {module:svgcanvas~logMatrix} logMatrix
         * @property {module:history.HistoryCommand} MoveElementCommand
         * @property {module:namespaces.NS} NS
         * @property {module:utilities.preventClickDefault} preventClickDefault
         * @property {module:history.HistoryCommand} RemoveElementCommand
         * @property {module:SVGTransformList.SVGEditTransformList} SVGEditTransformList
         * @property {module:utilities.text2xml} text2xml
         * @property {module:math.transformBox} transformBox
         * @property {module:math.transformPoint} transformPoint
         * @property {module:utilities.walkTree} walkTree
         */
        /**
* @deprecated getPrivateMethods
* Since all methods are/should be public somehow, this function should be removed;
*  we might require `import` in place of this in the future once ES6 Modules
*  widespread

* Being able to access private methods publicly seems wrong somehow,
* but currently appears to be the best way to allow testing and provide
* access to them to plugins.
* @function module:svgcanvas.SvgCanvas#getPrivateMethods
* @returns {module:svgcanvas.PrivateMethods}
*/
        this.getPrivateMethods = function() {
            const obj = {
                addCommandToHistory,
                BatchCommand,
                ChangeElementCommand,
                decode64,
                dropXMLInteralSubset,
                encode64,
                ffClone,
                findDefs,
                findDuplicateGradient,
                getElem,
                getPathBBox,
                getTypeMap,
                getUrlFromAttr,
                identifyLayers: draw.identifyLayers,
                InsertElementCommand,
                isChrome,
                isIdentity,
                isIE,
                logMatrix,
                MoveElementCommand,
                NS,
                preventClickDefault,
                RemoveElementCommand,
                SVGEditTransformList,
                text2xml,
                transformBox,
                transformPoint,
                walkTree
            }
            return obj
        }
    } // End constructor
} // End class

export default SvgCanvas
